From 6ab2ed43a9a59b626910f0517aada66a4121c48f Mon Sep 17 00:00:00 2001 From: capJavert Date: Fri, 15 Nov 2024 17:17:08 +0100 Subject: [PATCH 1/9] feat: read from content_preference source member --- bin/import.ts | 1 + seeds/ContentPreferenceSource.json | 340 ++++++++++++++++++ src/common/feedGenerator.ts | 20 +- src/common/pubsub.ts | 6 +- src/common/users.ts | 15 +- src/dataLoaderService.ts | 18 +- src/graphorm/index.ts | 26 +- src/routes/boot.ts | 32 +- src/schema/comments.ts | 6 +- src/schema/integrations.ts | 27 +- src/schema/sourceRequests.ts | 8 +- src/schema/sources.ts | 79 ++-- src/workers/cdc/primary.ts | 26 +- .../notifications/articleUpvoteMilestone.ts | 19 +- .../notifications/commentUpvoteMilestone.ts | 19 +- src/workers/notifications/postAdded.ts | 25 +- .../notifications/sourceMemberRoleChanged.ts | 10 +- .../sourcePostModerationSubmitted.ts | 19 +- .../notifications/squadFeaturedUpdated.ts | 14 +- .../notifications/squadMemberJoined.ts | 23 +- src/workers/notifications/utils.ts | 33 +- src/workers/postAddedSlackChannelSend.ts | 32 +- src/workers/sourceSquadCreatedUserAction.ts | 21 +- 23 files changed, 634 insertions(+), 185 deletions(-) create mode 100644 seeds/ContentPreferenceSource.json diff --git a/bin/import.ts b/bin/import.ts index a48acaaa1..b85e8b4b8 100644 --- a/bin/import.ts +++ b/bin/import.ts @@ -64,6 +64,7 @@ const start = async (): Promise => { await importEntity(con, 'UserTopReader'); await importEntity(con, 'MarketingCta'); await importEntity(con, 'SourceMember'); + await importEntity(con, 'ContentPreferenceSource'); // Manually have to reset these as insert has a issue with `type` columns await con.query(`update post set type = 'article' where type = 'Post'`); await con.query(`update source set type = 'machine' where type = 'Source'`); diff --git a/seeds/ContentPreferenceSource.json b/seeds/ContentPreferenceSource.json new file mode 100644 index 000000000..40e92788a --- /dev/null +++ b/seeds/ContentPreferenceSource.json @@ -0,0 +1,340 @@ +[ + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser", + "role": "admin", + "createdAt": "2024-07-01 10:48:13.433031", + "flags": { + "role": "admin", + "referralToken": "token1" + }, + "status": "subscribed", + "feedId": "testuser" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser1", + "role": "member", + "createdAt": "2024-07-02 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token2" + }, + "status": "subscribed", + "feedId": "testuser1" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser2", + "role": "member", + "createdAt": "2024-07-03 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token3" + }, + "status": "subscribed", + "feedId": "testuser2" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser3", + "role": "member", + "createdAt": "2024-07-04 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token4" + }, + "status": "subscribed", + "feedId": "testuser3" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser4", + "role": "member", + "createdAt": "2024-07-05 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token5" + }, + "status": "subscribed", + "feedId": "testuser4" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser5", + "role": "member", + "createdAt": "2024-07-06 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token6" + }, + "status": "subscribed", + "feedId": "testuser5" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser6", + "role": "member", + "createdAt": "2024-07-07 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token7" + }, + "status": "subscribed", + "feedId": "testuser6" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser7", + "role": "member", + "createdAt": "2024-07-08 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token8" + }, + "status": "subscribed", + "feedId": "testuser7" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser8", + "role": "member", + "createdAt": "2024-07-09 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token9" + }, + "status": "subscribed", + "feedId": "testuser8" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser9", + "role": "member", + "createdAt": "2024-07-10 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token10" + }, + "status": "subscribed", + "feedId": "testuser9" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser10", + "role": "member", + "createdAt": "2024-07-11 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token11" + }, + "status": "subscribed", + "feedId": "testuser10" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser11", + "role": "member", + "createdAt": "2024-07-12 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token12" + }, + "status": "subscribed", + "feedId": "testuser11" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser12", + "role": "member", + "createdAt": "2024-07-13 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token13" + }, + "status": "subscribed", + "feedId": "testuser12" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser13", + "role": "member", + "createdAt": "2024-07-14 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token14" + }, + "status": "subscribed", + "feedId": "testuser13" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser14", + "role": "member", + "createdAt": "2024-07-15 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token15" + }, + "status": "subscribed", + "feedId": "testuser14" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser15", + "role": "member", + "createdAt": "2024-07-16 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token16" + }, + "status": "subscribed", + "feedId": "testuser15" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser16", + "role": "member", + "createdAt": "2024-07-17 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token17" + }, + "status": "subscribed", + "feedId": "testuser16" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser17", + "role": "member", + "createdAt": "2024-07-18 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token18" + }, + "status": "subscribed", + "feedId": "testuser17" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser18", + "role": "member", + "createdAt": "2024-07-19 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token19" + }, + "status": "subscribed", + "feedId": "testuser18" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser19", + "role": "member", + "createdAt": "2024-07-20 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token20" + }, + "status": "subscribed", + "feedId": "testuser19" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser20", + "role": "member", + "createdAt": "2024-07-21 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token21" + }, + "status": "subscribed", + "feedId": "testuser20" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser21", + "role": "member", + "createdAt": "2024-07-22 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token22" + }, + "status": "subscribed", + "feedId": "testuser21" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser22", + "role": "member", + "createdAt": "2024-07-23 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token23" + }, + "status": "subscribed", + "feedId": "testuser22" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser23", + "role": "member", + "createdAt": "2024-07-24 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token24" + }, + "status": "subscribed", + "feedId": "testuser23" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser24", + "role": "member", + "createdAt": "2024-07-25 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token25" + }, + "status": "subscribed", + "feedId": "testuser24" + }, + { + "sourceId": "publicsquad", + "referenceId": "publicsquad", + "userId": "testuser25", + "role": "member", + "createdAt": "2024-07-26 10:48:13.433031", + "flags": { + "role": "member", + "referralToken": "token26" + }, + "status": "subscribed", + "feedId": "testuser25" + } +] diff --git a/src/common/feedGenerator.ts b/src/common/feedGenerator.ts index 08a6b2b8d..c77b64e9b 100644 --- a/src/common/feedGenerator.ts +++ b/src/common/feedGenerator.ts @@ -1,7 +1,6 @@ import { AdvancedSettings, FeedAdvancedSettings, - SourceMember, SourceType, UserPost, } from '../entity'; @@ -96,7 +95,9 @@ type RawFiltersData = { | null; tags: Pick[] | null; excludeSources: Pick[] | null; - memberships: { sourceId: SourceMember['sourceId']; hide: boolean }[] | null; + memberships: + | { sourceId: ContentPreferenceSource['sourceId']; hide: boolean }[] + | null; }; const getRawFiltersData = async ( @@ -136,9 +137,18 @@ const getRawFiltersData = async ( rawFilterSelect(con, 'memberships', (qb) => qb .select('"sourceId"') - .addSelect("COALESCE((flags->'hideFeedPosts')::boolean, FALSE)", 'hide') - .from(SourceMember, 't') - .where('"userId" = $2'), + .addSelect( + "COALESCE((t.flags->'hideFeedPosts')::boolean, FALSE)", + 'hide', + ) + .from(ContentPreference, 't') + .innerJoin( + Source, + 'source', + `t."sourceId" = source.id AND source.type = '${SourceType.Squad}'`, + ) + .where(`t.type = '${ContentPreferenceType.Source}'`) + .andWhere('"userId" = $2'), ), ]; const query = diff --git a/src/common/pubsub.ts b/src/common/pubsub.ts index b3cd95dc7..999b9b683 100644 --- a/src/common/pubsub.ts +++ b/src/common/pubsub.ts @@ -6,7 +6,6 @@ import { Submission, User, CommentMention, - SourceMember, Feature, Source, ArticlePost, @@ -34,6 +33,7 @@ import { FastifyLoggerInstance } from 'fastify'; import pino from 'pino'; import { PersonalizedDigestFeatureConfig } from '../growthbook'; import { Message as ProtobufMessage } from '@bufbuild/protobuf'; +import { ContentPreferenceSource } from '../entity/contentPreference/ContentPreferenceSource'; export const pubsub = new PubSub(); const postCommentedTopic = pubsub.topic('post-commented'); @@ -270,7 +270,7 @@ export const notifySubmissionGrantedAccess = async ( export const notifySourceMemberRoleChanged = async ( log: EventLogger, previousRole: SourceMemberRoles, - sourceMember: ChangeObject, + sourceMember: ChangeObject, ): Promise => publishEvent(log, sourceMemberRoleChangedTopic, { previousRole, @@ -295,7 +295,7 @@ export const notifyNewCommentMention = async ( export const notifyMemberJoinedSource = async ( log: EventLogger, - sourceMember: ChangeObject, + sourceMember: ChangeObject, ): Promise => publishEvent(log, memberJoinedSourceTopic, { sourceMember }); diff --git a/src/common/users.ts b/src/common/users.ts index cd2c58896..4cb0fb73c 100644 --- a/src/common/users.ts +++ b/src/common/users.ts @@ -8,7 +8,7 @@ import { } from '../entity'; import { differenceInDays, isSameDay, max, startOfDay } from 'date-fns'; import { DataSource, EntityManager, In, Not } from 'typeorm'; -import { CommentMention, Comment, View, Source, SourceMember } from '../entity'; +import { CommentMention, Comment, View, Source } from '../entity'; import { getTimezonedStartOfISOWeek, getTimezonedEndOfISOWeek } from './utils'; import { GraphQLResolveInfo } from 'graphql'; import { utcToZonedTime } from 'date-fns-tz'; @@ -20,6 +20,7 @@ import { queryReadReplica } from './queryReadReplica'; import { logger } from '../logger'; import type { GQLKeyword } from '../schema/keywords'; import type { GQLUser } from '../schema/users'; +import { ContentPreferenceSource } from '../entity/contentPreference/ContentPreferenceSource'; export interface User { id: string; @@ -163,8 +164,12 @@ export const getRecentMentionsIds = async ( if (sourceId) { queryBuilder = queryBuilder - .innerJoin(SourceMember, 'sm', 'sm."userId" = cm."mentionedUserId"') - .andWhere('sm."sourceId" = :sourceId', { sourceId }); + .innerJoin( + ContentPreferenceSource, + 'cps', + 'cps."userId" = cm."mentionedUserId"', + ) + .andWhere('cps."referenceId" = :sourceId', { sourceId }); } if (query) { @@ -205,8 +210,8 @@ export const getUserIdsByNameOrUsername = async ( if (sourceId) { queryBuilder = queryBuilder - .innerJoin(SourceMember, 'sm', 'id = sm."userId"') - .andWhere('sm."sourceId" = :sourceId', { sourceId }); + .innerJoin(ContentPreferenceSource, 'cps', 'id = cps."userId"') + .andWhere('cps."referenceId" = :sourceId', { sourceId }); } if (excludeIds?.length) { diff --git a/src/dataLoaderService.ts b/src/dataLoaderService.ts index 7da4dde05..1227acf31 100644 --- a/src/dataLoaderService.ts +++ b/src/dataLoaderService.ts @@ -1,8 +1,8 @@ import DataLoader, { BatchLoadFn } from 'dataloader'; import { Context } from './Context'; import { getShortUrl } from './common'; -import { SourceMember } from './entity'; import { GQLSource } from './schema/sources'; +import { ContentPreferenceSource } from './entity/contentPreference/ContentPreferenceSource'; export const defaultCacheKeyFn = (key: K) => { if (typeof key === 'object') { @@ -83,13 +83,15 @@ export class DataLoaderService { let referralToken = source.currentMember?.referralToken; if (!referralToken) { - const sourceMember: Pick | null = - await this.ctx.con.getRepository(SourceMember).findOne({ - select: ['referralToken'], - where: { sourceId: source.id, userId }, - }); - - referralToken = sourceMember?.referralToken; + const sourceMember: Pick | null = + await this.ctx.con + .getRepository(ContentPreferenceSource) + .findOne({ + select: ['flags'], + where: { referenceId: source.id, userId }, + }); + + referralToken = sourceMember?.flags.referralToken; } if (!referralToken) { diff --git a/src/graphorm/index.ts b/src/graphorm/index.ts index 83c30063c..7d3d2a119 100644 --- a/src/graphorm/index.ts +++ b/src/graphorm/index.ts @@ -3,7 +3,6 @@ import { GraphORM, GraphORMField, QueryBuilder } from './graphorm'; import { Bookmark, Source, - SourceMember, User, UserPost, UserNotification, @@ -455,8 +454,8 @@ const obj = new GraphORM({ sort: 'createdAt', customRelation: (ctx, parentAlias, childAlias, qb): QueryBuilder => qb - .where(`${childAlias}."sourceId" = "${parentAlias}".id`) - .andWhere(`${childAlias}."role" != :role`, { + .where(`${childAlias}."referenceId" = "${parentAlias}".id`) + .andWhere(`${childAlias}.flags->>'role' != :role`, { role: SourceMemberRoles.Blocked, }), }, @@ -487,8 +486,8 @@ const obj = new GraphORM({ isMany: true, customRelation: (ctx, parentAlias, childAlias, qb): QueryBuilder => { return qb - .where(`${childAlias}."sourceId" = "${parentAlias}".id`) - .andWhere(`${childAlias}.role IN (:...roles)`, { + .where(`${childAlias}."referenceId" = "${parentAlias}".id`) + .andWhere(`${childAlias}.flags->>'role' IN (:...roles)`, { roles: [SourceMemberRoles.Admin, SourceMemberRoles.Moderator], }) .limit(50); // limit to avoid huge arrays for members, most sources should fit into this see PR !1219 for more info @@ -509,7 +508,10 @@ const obj = new GraphORM({ }, }, SourceMember: { - requiredColumns: ['createdAt', 'userId', 'role'], + from: 'ContentPreference', + additionalQuery: (ctx, alias, qb) => + qb.andWhere(`"${alias}"."type" = '${ContentPreferenceType.Source}'`), + requiredColumns: ['createdAt', 'userId', 'flags'], fields: { permissions: { select: (ctx: Context, alias: string, qb: QueryBuilder): string => { @@ -520,7 +522,7 @@ const obj = new GraphORM({ return `${query.getQuery()}`; }, transform: (value: [number, number], ctx: Context, parent) => { - const member = parent as SourceMember; + const member = parent as ContentPreferenceSource; if (!ctx.userId || member.userId !== ctx.userId) { return null; @@ -541,19 +543,25 @@ const obj = new GraphORM({ ${sourceRoleRankKeys .map( (role) => - `WHEN "role" = '${role}' THEN ${sourceRoleRank[role as keyof typeof sourceRoleRank]}`, + `WHEN flags->>'role' = '${role}' THEN ${sourceRoleRank[role as keyof typeof sourceRoleRank]}`, ) .join(' ')} ELSE 0 END) `, }, referralToken: { + rawSelect: true, + select: `flags->>'referralToken'`, transform: (value: string, ctx: Context, parent) => { - const member = parent as SourceMember; + const member = parent as ContentPreferenceSource; return nullIfNotSameUser(value, ctx, { id: member.userId }); }, }, + role: { + rawSelect: true, + select: `COALESCE(flags->>'role', '${SourceMemberRoles.Member}')`, + }, flags: { jsonType: true, }, diff --git a/src/routes/boot.ts b/src/routes/boot.ts index 2454da0c0..ee3b8941f 100644 --- a/src/routes/boot.ts +++ b/src/routes/boot.ts @@ -14,7 +14,7 @@ import { MarketingCta, Settings, SETTINGS_DEFAULT, - SourceMember, + SourceType, SquadSource, User, } from '../entity'; @@ -63,6 +63,10 @@ import { maxFeedsPerUser } from '../types'; import { queryReadReplica } from '../common/queryReadReplica'; import { queryDataSource } from '../common/queryDataSource'; import { isPlusMember } from '../paddle'; +import { + ContentPreferenceSource, + ContentPreferenceSourceFlags, +} from '../entity/contentPreference/ContentPreferenceSource'; export type BootSquadSource = Omit & { permalink: string; @@ -169,33 +173,41 @@ const getSquads = async ( const sources = await con.manager .createQueryBuilder() .select('id') - .addSelect('type') + .addSelect('s.type', 'type') .addSelect('name') .addSelect('handle') .addSelect('image') .addSelect('NOT private', 'public') .addSelect('active') - .addSelect('role') + .addSelect('cps.flags', 'flags') .addSelect('"moderationRequired"') .addSelect('"memberPostingRank"') - .from(SourceMember, 'sm') + .from(ContentPreferenceSource, 'cps') .innerJoin( SquadSource, 's', - 'sm."sourceId" = s."id" and s."type" = \'squad\'', + 'cps."referenceId" = s."id" and s."type" = :sourceType', + { + sourceType: SourceType.Squad, + }, ) - .where('sm."userId" = :userId', { userId }) - .andWhere('sm."role" != :role', { role: SourceMemberRoles.Blocked }) + .where('cps."userId" = :userId', { userId }) + .andWhere(`cps.flags->>'role' != :role`, { + role: SourceMemberRoles.Blocked, + }) .orderBy('LOWER(s.name)', 'ASC') .getRawMany< - GQLSource & { role: SourceMemberRoles; memberPostingRank: number } + GQLSource & { + flags: ContentPreferenceSourceFlags; + memberPostingRank: number; + } >(); return sources.map((source) => { - const { role, memberPostingRank, image, ...restSource } = source; + const { flags, memberPostingRank, image, ...restSource } = source; const permissions = getPermissionsForMember( - { role }, + { flags }, { memberPostingRank }, ); // we only send posting and moderation permissions from boot to keep the payload small diff --git a/src/schema/comments.ts b/src/schema/comments.ts index 40a343443..586837ece 100644 --- a/src/schema/comments.ts +++ b/src/schema/comments.ts @@ -15,7 +15,6 @@ import { CommentMention, Post, Source, - SourceMember, SourceType, User, PostType, @@ -52,6 +51,7 @@ import { import { reportComment } from '../common/reporting'; import { ReportReason } from '../entity/common'; import { toGQLEnum } from '../common/utils'; +import { ContentPreferenceSource } from '../entity/contentPreference/ContentPreferenceSource'; export interface GQLComment { id: string; @@ -481,8 +481,8 @@ export const getMentions = async ( return repo .createQueryBuilder('u') .select('u.id, u.username') - .innerJoin(SourceMember, 'sm', 'u.id = sm."userId"') - .where('sm."sourceId" = :sourceId', { sourceId }) + .innerJoin(ContentPreferenceSource, 'cps', 'u.id = cps."userId"') + .where('cps."referenceId" = :sourceId', { sourceId }) .andWhere('u.username IN (:...usernames)', { usernames: result }) .andWhere('u.id != :id', { id: userId }) .getRawMany(); diff --git a/src/schema/integrations.ts b/src/schema/integrations.ts index 09749cdd2..605d58092 100644 --- a/src/schema/integrations.ts +++ b/src/schema/integrations.ts @@ -31,13 +31,14 @@ import { GQLSource, SourcePermissions, } from './sources'; -import { SourceMember, SourceType } from '../entity'; +import { SourceType } from '../entity'; import { Connection, ConnectionArguments } from 'graphql-relay'; import { GQLDatePageGeneratorConfig, queryPaginatedByDate, } from '../common/datePageGenerator'; import { ConversationsInfoResponse } from '@slack/web-api'; +import { ContentPreferenceSource } from '../entity/contentPreference/ContentPreferenceSource'; import { SourceMemberRoles } from '../roles'; export type GQLSlackChannels = { @@ -350,16 +351,18 @@ export const resolvers: IResolvers = traceResolvers< }, }), source.private && source.type === SourceType.Squad - ? ctx.con.getRepository(SourceMember).findOne({ - select: ['referralToken'], - where: { - sourceId: source.id, + ? ctx.con + .getRepository(ContentPreferenceSource) + .createQueryBuilder() + .select('flags') + .where('"sourceId" = :sourceId', { + sourceId: args.sourceId, + }) + .andWhere(`flags->>'role' = :role`, { role: SourceMemberRoles.Admin, - }, - order: { - createdAt: 'ASC', - }, - }) + }) + .orderBy('"createdAt"', 'ASC') + .getOne() : null, ]); const user = await slackIntegration.user; @@ -437,11 +440,11 @@ export const resolvers: IResolvers = traceResolvers< 'connected', ); - if (sourceAdmin?.referralToken) { + if (sourceAdmin?.flags.referralToken) { squadLink = addPrivateSourceJoinParams({ url: squadLink, source, - referralToken: sourceAdmin?.referralToken, + referralToken: sourceAdmin?.flags.referralToken, }); } diff --git a/src/schema/sourceRequests.ts b/src/schema/sourceRequests.ts index 542c126e6..84cae1196 100644 --- a/src/schema/sourceRequests.ts +++ b/src/schema/sourceRequests.ts @@ -15,7 +15,6 @@ import { MachineSource, Source, SourceFeed, - SourceMember, SourceRequest, SquadPublicRequest, SquadPublicRequestStatus, @@ -34,6 +33,7 @@ import { import { ConnectionArguments } from 'graphql-relay'; import { SourceMemberRoles } from '../roles'; import { MoreThan } from 'typeorm'; +import { ContentPreferenceSource } from '../entity/contentPreference/ContentPreferenceSource'; export interface GQLSourceRequest { id: string; @@ -453,11 +453,11 @@ const ensureSourceRole = async ( .findOneByOrFail([{ id: sourceId }, { handle: sourceId }]); const sourceMember = ctx.userId ? await ctx.con - .getRepository(SourceMember) - .findOneBy({ sourceId: source.id, userId: ctx.userId }) + .getRepository(ContentPreferenceSource) + .findOneBy({ referenceId: source.id, userId: ctx.userId }) : null; - if (!sourceMember || sourceMember.role !== role) { + if (!sourceMember || sourceMember.flags.role !== role) { throw new ForbiddenError('Access denied!'); } diff --git a/src/schema/sources.ts b/src/schema/sources.ts index caaac0a61..643a9e2ea 100644 --- a/src/schema/sources.ts +++ b/src/schema/sources.ts @@ -913,18 +913,22 @@ const requireGreaterAccessPrivilege: Partial< [SourcePermissions.MemberRemove]: true, }; -type BaseSourceMember = Pick; +type BaseSourceMember = Pick; export const hasGreaterAccessCheck = ( loggedUser: BaseSourceMember, member: BaseSourceMember, ) => { - if (loggedUser.role === SourceMemberRoles.Admin) { + const loggedUserRole = loggedUser.flags.role || SourceMemberRoles.Member; + + if (loggedUserRole === SourceMemberRoles.Admin) { return; } - const memberRank = sourceRoleRank[member.role]; - const loggedUserRank = sourceRoleRank[loggedUser.role]; + const memberRole = member.flags.role || SourceMemberRoles.Member; + + const memberRank = sourceRoleRank[memberRole]; + const loggedUserRank = sourceRoleRank[loggedUserRole]; const hasGreaterAccess = loggedUserRank > memberRank; if (!hasGreaterAccess) { @@ -942,7 +946,8 @@ const hasPermissionCheck = ( hasGreaterAccessCheck(member, validateRankAgainst); } - const rolePermissions = roleSourcePermissions[member.role]; + const rolePermissions = + roleSourcePermissions[member.flags.role || SourceMemberRoles.Member]; return rolePermissions?.includes?.(permission); }; @@ -952,13 +957,13 @@ export const sourceTypesWithMembers = ['squad']; export const canAccessSource = async ( ctx: Context, source: Source, - member: SourceMember | null, + member: ContentPreferenceSource | null, permission: SourcePermissions, validateRankAgainstId?: string, ): Promise => { if (permission === SourcePermissions.View && !source.private) { if (sourceTypesWithMembers.includes(source.type)) { - const isMemberBlocked = member?.role === SourceMemberRoles.Blocked; + const isMemberBlocked = member?.flags.role === SourceMemberRoles.Blocked; const canAccess = !isMemberBlocked; return canAccess; @@ -972,9 +977,12 @@ export const canAccessSource = async ( } const sourceId = source.id; - const repo = ctx.getRepository(SourceMember); + const repo = ctx.getRepository(ContentPreferenceSource); const validateRankAgainst = await (requireGreaterAccessPrivilege[permission] - ? repo.findOneByOrFail({ sourceId, userId: validateRankAgainstId }) + ? repo.findOneByOrFail({ + referenceId: sourceId, + userId: validateRankAgainstId, + }) : Promise.resolve(undefined)); return hasPermissionCheck(source, member, permission, validateRankAgainst); @@ -985,13 +993,16 @@ export const isPrivilegedMember = async ( sourceId: string, ): Promise => { const sourceMember = await ctx.con - .getRepository(SourceMember) - .findOneBy({ sourceId: sourceId, userId: ctx.userId }); + .getRepository(ContentPreferenceSource) + .findOneBy({ referenceId: sourceId, userId: ctx.userId }); if (!sourceMember) throw new ForbiddenError(SourceRequestErrorMessage.ACCESS_DENIED); - return sourceRoleRank[sourceMember.role] >= sourceRoleRank.moderator; + return ( + sourceRoleRank[sourceMember.flags.role || SourceMemberRoles.Member] >= + sourceRoleRank.moderator + ); }; type PostPermissions = SourcePermissions.Post | SourcePermissions.PostRequest; @@ -999,14 +1010,15 @@ type PostPermissions = SourcePermissions.Post | SourcePermissions.PostRequest; export const canPostToSquad = ( ctx: Context, squad: SquadSource, - sourceMember: SourceMember | null, + sourceMember: ContentPreferenceSource | null, permission: PostPermissions = SourcePermissions.Post, ): boolean => { if (!sourceMember) { return false; } - const memberRank = sourceRoleRank[sourceMember.role]; + const memberRank = + sourceRoleRank[sourceMember.flags.role || SourceMemberRoles.Member]; if (squad.moderationRequired) { if (memberRank === sourceRoleRank.member) { @@ -1086,8 +1098,8 @@ export const ensureSourcePermissions = async ( .findOneByOrFail([{ id: sourceId }, { handle: sourceId }]); const sourceMember = ctx.userId ? await ctx.con - .getRepository(SourceMember) - .findOneBy({ sourceId: source.id, userId: ctx.userId }) + .getRepository(ContentPreferenceSource) + .findOneBy({ referenceId: source.id, userId: ctx.userId }) : null; const canAccess = await canAccessSource( @@ -1216,13 +1228,15 @@ const addNewSourceMember = async ( }; export const getPermissionsForMember = ( - member: Pick, + member: Pick, source: Partial>, ): SourcePermissions[] => { + const memberRole = member.flags.role || SourceMemberRoles.Member; + const permissions = - roleSourcePermissions[member.role] ?? roleSourcePermissions.member; + roleSourcePermissions[memberRole] ?? roleSourcePermissions.member; const memberRank = - sourceRoleRank[member.role] ?? sourceRoleRank[SourceMemberRoles.Member]; + sourceRoleRank[memberRole] ?? sourceRoleRank[SourceMemberRoles.Member]; const permissionsToRemove: SourcePermissions[] = []; if (source.memberPostingRank && memberRank < source.memberPostingRank) { @@ -1744,7 +1758,7 @@ export const resolvers: IResolvers = traceResolvers< return paginateSourceMembers( (queryBuilder, alias) => { queryBuilder = queryBuilder.andWhere( - `${alias}."sourceId" = :source`, + `${alias}."referenceId" = :source`, { source: sourceId, }, @@ -1774,9 +1788,12 @@ export const resolvers: IResolvers = traceResolvers< } if (role) { - queryBuilder = queryBuilder.andWhere(`${alias}.role = :role`, { - role, - }); + queryBuilder = queryBuilder.andWhere( + `${alias}.flags->>'role' = :role`, + { + role, + }, + ); } else if ( typeof graphorm.mappings?.SourceMember.fields?.roleRank.select === 'string' @@ -1822,7 +1839,7 @@ export const resolvers: IResolvers = traceResolvers< if (type) { queryBuilder = queryBuilder - .innerJoin(Source, 's', `${alias}."sourceId" = s.id`) + .innerJoin(Source, 's', `${alias}."referenceId" = s.id`) .andWhere(`s."type" = :type`, { type, }); @@ -1862,7 +1879,7 @@ export const resolvers: IResolvers = traceResolvers< return queryBuilder .addOrderBy(`${alias}."createdAt"`, 'DESC') - .innerJoin(Source, 's', `${alias}."sourceId" = s.id`) + .innerJoin(Source, 's', `${alias}."referenceId" = s.id`) .andWhere('s.private = false'); }, args, @@ -1881,14 +1898,14 @@ export const resolvers: IResolvers = traceResolvers< info, (builder) => { builder.queryBuilder = builder.queryBuilder - .andWhere({ referralToken: token }) + .andWhere(`flags->>'referralToken' = :token`, { token }) .limit(1); return builder; }, true, ); if (!res.length) { - throw new EntityNotFoundError(SourceMember, 'not found'); + throw new EntityNotFoundError(ContentPreferenceSource, 'not found'); } return res[0]; }, @@ -2239,8 +2256,10 @@ export const resolvers: IResolvers = traceResolvers< } const member = await ctx.con - .getRepository(SourceMember) - .findOneBy({ referralToken: token }); + .getRepository(ContentPreferenceSource) + .createQueryBuilder() + .where(`flags->>'referralToken' = :token`, { token }) + .getOne(); if (!member) { throw new ForbiddenError( @@ -2249,7 +2268,7 @@ export const resolvers: IResolvers = traceResolvers< } const memberRank = - sourceRoleRank[member.role] ?? + sourceRoleRank[member.flags.role || SourceMemberRoles.Member] ?? sourceRoleRank[SourceMemberRoles.Member]; const squadSource = source as SquadSource; diff --git a/src/workers/cdc/primary.ts b/src/workers/cdc/primary.ts index 7f6890222..fbf1dfda6 100644 --- a/src/workers/cdc/primary.ts +++ b/src/workers/cdc/primary.ts @@ -11,6 +11,7 @@ import { Alerts, UserTopReader, SquadSource, + SourceType, } from '../../entity'; import { messageToJson, Worker } from '../worker'; import { @@ -20,7 +21,6 @@ import { Post, Settings, SourceFeed, - SourceMember, SourceRequest, Submission, SubmissionStatus, @@ -124,6 +124,8 @@ import { SourcePostModeration, SourcePostModerationStatus, } from '../../entity/SourcePostModeration'; +import { ContentPreferenceSource } from '../../entity/contentPreference/ContentPreferenceSource'; +import { ContentPreference } from '../../entity/contentPreference/ContentPreference'; const isFreeformPostLongEnough = ( freeform: ChangeMessage, @@ -835,16 +837,30 @@ const onUserStateChange = async ( const onSourceMemberChange = async ( con: DataSource, logger: FastifyBaseLogger, - data: ChangeMessage, + data: ChangeMessage, ) => { + const sourceId = + data.payload.after!.sourceId || data.payload.before!.sourceId; + + const source = await con.getRepository(Source).findOne({ + select: ['id'], + where: { + id: sourceId, + }, + }); + + if (source?.type !== SourceType.Squad) { + return; + } + if (data.payload.op === 'c') { await notifyMemberJoinedSource(logger, data.payload.after!); } if (data.payload.op === 'u') { - if (data.payload.before!.role !== data.payload.after!.role) { + if (data.payload.before!.flags.role !== data.payload.after!.flags.role) { await notifySourceMemberRoleChanged( logger, - data.payload.before!.role, + data.payload.before!.flags.role!, data.payload.after!, ); } @@ -1148,7 +1164,7 @@ const worker: Worker = { case getTableName(con, UserState): await onUserStateChange(con, logger, data); break; - case getTableName(con, SourceMember): + case getTableName(con, ContentPreference): await onSourceMemberChange(con, logger, data); break; case getTableName(con, Feature): diff --git a/src/workers/notifications/articleUpvoteMilestone.ts b/src/workers/notifications/articleUpvoteMilestone.ts index abc121ffc..c588b2cdc 100644 --- a/src/workers/notifications/articleUpvoteMilestone.ts +++ b/src/workers/notifications/articleUpvoteMilestone.ts @@ -1,5 +1,5 @@ import { messageToJson } from '../worker'; -import { SourceMember, SourceType, UserPost } from '../../entity'; +import { SourceType, UserPost } from '../../entity'; import { NotificationPostContext, NotificationUpvotersContext, @@ -7,9 +7,9 @@ import { import { NotificationType } from '../../notifications/common'; import { NotificationWorker } from './worker'; import { buildPostContext, uniquePostOwners, UPVOTE_MILESTONES } from './utils'; -import { In, Not } from 'typeorm'; import { SourceMemberRoles } from '../../roles'; import { UserVote } from '../../types'; +import { ContentPreferenceSource } from '../../entity/contentPreference/ContentPreferenceSource'; interface Data { userId: string; @@ -46,11 +46,16 @@ const worker: NotificationWorker = { }; if (source.type === SourceType.Squad) { - const members = await con.getRepository(SourceMember).findBy({ - userId: In(users), - sourceId: source.id, - role: Not(SourceMemberRoles.Blocked), - }); + const members = await con + .getRepository(ContentPreferenceSource) + .createQueryBuilder() + .select('"userId"') + .where('"userId" IN (:...users)', { users }) + .where('"referenceId" = :sourceId', { sourceId: source.id }) + .andWhere("flags->>'role' != :role", { + role: SourceMemberRoles.Blocked, + }) + .getMany(); if (!members.length) { return; diff --git a/src/workers/notifications/commentUpvoteMilestone.ts b/src/workers/notifications/commentUpvoteMilestone.ts index 93bbb0554..bd187b3fc 100644 --- a/src/workers/notifications/commentUpvoteMilestone.ts +++ b/src/workers/notifications/commentUpvoteMilestone.ts @@ -1,5 +1,5 @@ import { messageToJson } from '../worker'; -import { Comment, SourceMember, SourceType } from '../../entity'; +import { Comment, SourceType } from '../../entity'; import { NotificationCommentContext, NotificationUpvotersContext, @@ -7,10 +7,10 @@ import { import { NotificationType } from '../../notifications/common'; import { NotificationWorker } from './worker'; import { buildPostContext, UPVOTE_MILESTONES } from './utils'; -import { Not } from 'typeorm'; import { SourceMemberRoles } from '../../roles'; import { UserComment } from '../../entity/user/UserComment'; import { UserVote } from '../../types'; +import { ContentPreferenceSource } from '../../entity/contentPreference/ContentPreferenceSource'; interface Data { userId: string; @@ -52,11 +52,16 @@ const worker: NotificationWorker = { }; if (source.type === SourceType.Squad) { - const member = await con.getRepository(SourceMember).findOneBy({ - userId: comment.userId, - sourceId: source.id, - role: Not(SourceMemberRoles.Blocked), - }); + const member = await con + .getRepository(ContentPreferenceSource) + .createQueryBuilder() + .select('"userId"') + .where('"userId" = :userId', { userId: comment.userId }) + .andWhere('"referenceId" = :sourceId', { sourceId: source.id }) + .andWhere(`flags->>'role' != :role`, { + role: SourceMemberRoles.Blocked, + }) + .getOne(); if (!member) { return; diff --git a/src/workers/notifications/postAdded.ts b/src/workers/notifications/postAdded.ts index 059267353..82d31248e 100644 --- a/src/workers/notifications/postAdded.ts +++ b/src/workers/notifications/postAdded.ts @@ -21,7 +21,6 @@ import { import { NotificationHandlerReturn, NotificationWorker } from './worker'; import { ChangeObject } from '../../types'; import { buildPostContext, getSubscribedMembers } from './utils'; -import { In, Not } from 'typeorm'; import { SourceMemberRoles } from '../../roles'; import { insertOrIgnoreAction } from '../../schema/actions'; @@ -82,16 +81,20 @@ const worker: NotificationWorker = { con, NotificationType.SquadPostAdded, source.id, - { - sourceId: source.id, - userId: Not( - In([ - post.authorId, - ...mentions.flatMap(({ mentionedUserId }) => mentionedUserId), - ]), - ), - role: Not(SourceMemberRoles.Blocked), - }, + (qb) => + qb + .where(`${qb.alias}."userId" NOT IN (:...users)`, { + users: [ + post.authorId, + ...mentions.flatMap(({ mentionedUserId }) => mentionedUserId), + ], + }) + .andWhere(`${qb.alias}."referenceId" = :sourceId`, { + sourceId: source.id, + }) + .andWhere(` ${qb.alias}.flags->>'role' != :role`, { + role: SourceMemberRoles.Blocked, + }), ); if (members.length) { notifs.push({ diff --git a/src/workers/notifications/sourceMemberRoleChanged.ts b/src/workers/notifications/sourceMemberRoleChanged.ts index 27fa2fd7a..0073199d7 100644 --- a/src/workers/notifications/sourceMemberRoleChanged.ts +++ b/src/workers/notifications/sourceMemberRoleChanged.ts @@ -2,13 +2,14 @@ import { messageToJson } from '../worker'; import { NotificationSourceContext } from '../../notifications'; import { NotificationWorker } from './worker'; import { ChangeObject } from '../../types'; -import { Source, SourceMember } from '../../entity'; +import { Source } from '../../entity'; import { SourceMemberRoles } from '../../roles'; import { NotificationType } from '../../notifications/common'; +import { ContentPreferenceSource } from '../../entity/contentPreference/ContentPreferenceSource'; interface Data { previousRole: SourceMemberRoles; - sourceMember: ChangeObject; + sourceMember: ChangeObject; } const previousRoleToNewRole: Partial< @@ -55,8 +56,9 @@ const worker: NotificationWorker = { source, }; - const roleToNotificationMap = - previousRoleToNewRole[previousRole]?.[member.role]; + const roleToNotificationMap = member.flags.role + ? previousRoleToNewRole[previousRole]?.[member.flags.role] + : undefined; switch (roleToNotificationMap) { case 'demoted_to_member': diff --git a/src/workers/notifications/sourcePostModerationSubmitted.ts b/src/workers/notifications/sourcePostModerationSubmitted.ts index de8a81c59..58a3ecae2 100644 --- a/src/workers/notifications/sourcePostModerationSubmitted.ts +++ b/src/workers/notifications/sourcePostModerationSubmitted.ts @@ -1,6 +1,4 @@ import { generateTypedNotificationWorker } from './worker'; -import { SourceMember } from '../../entity'; -import { In } from 'typeorm'; import { SourceMemberRoles } from '../../roles'; import { NotificationType } from '../../notifications/common'; import { NotificationPostModerationContext } from '../../notifications'; @@ -8,6 +6,7 @@ import { SourcePostModerationStatus } from '../../entity/SourcePostModeration'; import { getPostModerationContext } from './utils'; import { logger } from '../../logger'; import { TypeORMQueryFailedError } from '../../errors'; +import { ContentPreferenceSource } from '../../entity/contentPreference/ContentPreferenceSource'; const worker = generateTypedNotificationWorker<'api.v1.source-post-moderation-submitted'>({ @@ -19,13 +18,15 @@ const worker = try { const moderationCtx = await getPostModerationContext(con, post); - const mods = await con.getRepository(SourceMember).find({ - select: ['userId'], - where: { - sourceId: post.sourceId, - role: In([SourceMemberRoles.Admin, SourceMemberRoles.Moderator]), - }, - }); + const mods = await con + .getRepository(ContentPreferenceSource) + .createQueryBuilder() + .select('"userId"') + .where('"referenceId" = :sourceId', { sourceId: post.sourceId }) + .andWhere(`flags->>'role' IN (:...roles)`, { + roles: [SourceMemberRoles.Admin, SourceMemberRoles.Moderator], + }) + .getMany(); const ctx: NotificationPostModerationContext = { ...moderationCtx, diff --git a/src/workers/notifications/squadFeaturedUpdated.ts b/src/workers/notifications/squadFeaturedUpdated.ts index ef8d07a57..58c59c440 100644 --- a/src/workers/notifications/squadFeaturedUpdated.ts +++ b/src/workers/notifications/squadFeaturedUpdated.ts @@ -2,8 +2,7 @@ import { NotificationType } from '../../notifications/common'; import { generateTypedNotificationWorker } from './worker'; import { NotificationSourceContext } from '../../notifications'; import { SourceMemberRoles } from '../../roles'; -import { SourceMember } from '../../entity'; -import { In } from 'typeorm'; +import { ContentPreferenceSource } from '../../entity/contentPreference/ContentPreferenceSource'; const toNotify = [SourceMemberRoles.Admin, SourceMemberRoles.Moderator]; @@ -15,10 +14,13 @@ const worker = generateTypedNotificationWorker<'api.v1.squad-featured-updated'>( return undefined; } - const users = await con.getRepository(SourceMember).findBy({ - sourceId: squad.id, - role: In(toNotify), - }); + const users = await con + .getRepository(ContentPreferenceSource) + .createQueryBuilder() + .select('"userId"') + .where('"referenceId" = :sourceId', { sourceId: squad.id }) + .andWhere(`flags->>'role' IN (:...roles)`, { roles: toNotify }) + .getMany(); if (!users.length) { return undefined; diff --git a/src/workers/notifications/squadMemberJoined.ts b/src/workers/notifications/squadMemberJoined.ts index 3c8a8dbac..7f8832d94 100644 --- a/src/workers/notifications/squadMemberJoined.ts +++ b/src/workers/notifications/squadMemberJoined.ts @@ -4,19 +4,18 @@ import { NotificationWorker } from './worker'; import { ChangeObject } from '../../types'; import { Source, - SourceMember, SourceType, User, UserActionType, WelcomePost, } from '../../entity'; -import { In, Not } from 'typeorm'; import { SourceMemberRoles } from '../../roles'; import { insertOrIgnoreAction } from '../../schema/actions'; import { getSubscribedMembers } from './utils'; +import { ContentPreferenceSource } from '../../entity/contentPreference/ContentPreferenceSource'; interface Data { - sourceMember: ChangeObject; + sourceMember: ChangeObject; } const worker: NotificationWorker = { @@ -28,11 +27,17 @@ const worker: NotificationWorker = { con, NotificationType.SquadMemberJoined, member.sourceId, - { - sourceId: member.sourceId, - userId: Not(In([member.userId])), - role: SourceMemberRoles.Admin, - }, + (qb) => + qb + .where(`${qb.alias}."userId" NOT IN (:...users)`, { + users: [member.userId], + }) + .andWhere(`${qb.alias}."referenceId" = :sourceId`, { + sourceId: member.sourceId, + }) + .andWhere(` ${qb.alias}.flags->>'role' = :role`, { + role: SourceMemberRoles.Admin, + }), ); const doneBy = await con @@ -45,7 +50,7 @@ const worker: NotificationWorker = { return; } - if (member.role !== SourceMemberRoles.Admin) { + if (member.flags.role !== SourceMemberRoles.Admin) { await insertOrIgnoreAction(con, member.userId, UserActionType.JoinSquad); } diff --git a/src/workers/notifications/utils.ts b/src/workers/notifications/utils.ts index 7e720cfbd..64b2f8680 100644 --- a/src/workers/notifications/utils.ts +++ b/src/workers/notifications/utils.ts @@ -7,7 +7,6 @@ import { PostType, SharePost, Source, - SourceMember, SourceType, User, UserActionType, @@ -22,12 +21,13 @@ import { NotificationPreferenceStatus, NotificationType, } from '../../notifications/common'; -import { DataSource, EntityManager, In, Not } from 'typeorm'; +import { DataSource, EntityManager, In, SelectQueryBuilder } from 'typeorm'; import { SourceMemberRoles } from '../../roles'; import { insertOrIgnoreAction } from '../../schema/actions'; import { ObjectLiteral } from 'typeorm/common/ObjectLiteral'; import { SourcePostModeration } from '../../entity/SourcePostModeration'; import { ChangeObject } from '../../types'; +import { ContentPreferenceSource } from '../../entity/contentPreference/ContentPreferenceSource'; export const uniquePostOwners = ( post: Pick, @@ -37,13 +37,19 @@ export const uniquePostOwners = ( (userId) => userId && !ignoreIds.includes(userId), ) as string[]; +type GetSubscribedMembersWhereBuilder = ( + qb: SelectQueryBuilder, +) => string; + export const getSubscribedMembers = ( con: DataSource, type: NotificationType, referenceId: string, - where: ObjectLiteral, + where: ObjectLiteral | GetSubscribedMembersWhereBuilder, ) => { - const builder = con.getRepository(SourceMember).createQueryBuilder('sm'); + const builder = con + .getRepository(ContentPreferenceSource) + .createQueryBuilder('cps'); const memberQuery = builder.select('"userId"').where(where); const muteQuery = builder .subQuery() @@ -59,7 +65,7 @@ export const getSubscribedMembers = ( return memberQuery .andWhere(`EXISTS(${muteQuery.getQuery()}) IS FALSE`) - .getRawMany(); + .getRawMany(); }; export const buildPostContext = async ( @@ -151,11 +157,18 @@ export async function articleNewCommentHandler( : NotificationType.ArticleNewComment; if (source.type === SourceType.Squad) { - const members = await getSubscribedMembers(con, type, post.id, { - userId: In(users), - sourceId: source.id, - role: Not(SourceMemberRoles.Blocked), - }); + const members = await getSubscribedMembers(con, type, post.id, (qb) => + qb + .where(`${qb.alias}."userId" IN (:...users)`, { + users, + }) + .andWhere(`${qb.alias}."referenceId" = :sourceId`, { + sourceId: source.id, + }) + .andWhere(` ${qb.alias}.flags->>'role' != :role`, { + role: SourceMemberRoles.Blocked, + }), + ); if (!members.length) { return; diff --git a/src/workers/postAddedSlackChannelSend.ts b/src/workers/postAddedSlackChannelSend.ts index 8f4cd2405..ea02827f0 100644 --- a/src/workers/postAddedSlackChannelSend.ts +++ b/src/workers/postAddedSlackChannelSend.ts @@ -1,15 +1,16 @@ import { UserSourceIntegrationSlack } from '../entity/UserSourceIntegration'; import { TypedWorker } from './worker'; import fastq from 'fastq'; -import { Post, SourceMember, SourceType } from '../entity'; +import { Post, SourceType } from '../entity'; import { getAttachmentForPostType, getSlackClient, } from '../common/userIntegration'; import { addNotificationUtm, addPrivateSourceJoinParams } from '../common'; -import { SourceMemberRoles } from '../roles'; import { SlackApiError, SlackApiErrorCode } from '../errors'; import { counters } from '../telemetry/metrics'; +import { ContentPreferenceSource } from '../entity/contentPreference/ContentPreferenceSource'; +import { SourceMemberRoles } from '../roles'; const sendQueueConcurrency = 10; @@ -61,24 +62,21 @@ export const postAddedSlackChannelSendWorker: TypedWorker<'api.v1.post-visible'> ); if (source.private && source.type === SourceType.Squad) { - const admin: Pick | null = await con - .getRepository(SourceMember) - .findOne({ - select: ['referralToken'], - where: { - sourceId: source.id, - role: SourceMemberRoles.Admin, - }, - order: { - createdAt: 'ASC', - }, - }); - - if (admin?.referralToken) { + const admin: Pick | null = await con + .getRepository(ContentPreferenceSource) + .createQueryBuilder() + .where('"referenceId" = :sourceId', { sourceId: source.id }) + .andWhere(`flags->>'role' = :role`, { + role: SourceMemberRoles.Admin, + }) + .orderBy('"createdAt"', 'ASC') + .getOne(); + + if (admin?.flags.referralToken) { postLink = addPrivateSourceJoinParams({ url: postLink, source, - referralToken: admin.referralToken, + referralToken: admin.flags.referralToken, }); } } diff --git a/src/workers/sourceSquadCreatedUserAction.ts b/src/workers/sourceSquadCreatedUserAction.ts index 0cc8996ba..a3177b71d 100644 --- a/src/workers/sourceSquadCreatedUserAction.ts +++ b/src/workers/sourceSquadCreatedUserAction.ts @@ -1,4 +1,5 @@ -import { Source, SourceMember, SourceType, UserActionType } from '../entity'; +import { Source, SourceType, UserActionType } from '../entity'; +import { ContentPreferenceSource } from '../entity/contentPreference/ContentPreferenceSource'; import { SourceMemberRoles } from '../roles'; import { insertOrIgnoreAction } from '../schema/actions'; import { ChangeObject } from '../types'; @@ -19,16 +20,14 @@ const worker: Worker = { return; } - const owner = await con.getRepository(SourceMember).findOne({ - select: ['userId'], - where: { - sourceId: source.id, - role: SourceMemberRoles.Admin, - }, - order: { - createdAt: 'ASC', - }, - }); + const owner = await con + .getRepository(ContentPreferenceSource) + .createQueryBuilder() + .select('"userId"') + .where('"referenceId" = :sourceId', { sourceId: source.id }) + .andWhere(`flags->>'role' = :role`, { role: SourceMemberRoles.Admin }) + .orderBy('"createdAt"', 'ASC') + .getOne(); if (!owner) { return; From fdcb7f3545c001fcbed40786fb05ed8114ae7c62 Mon Sep 17 00:00:00 2001 From: capJavert Date: Fri, 15 Nov 2024 17:52:54 +0100 Subject: [PATCH 2/9] fix: raw query when selecting --- src/schema/integrations.ts | 4 ++-- src/workers/notifications/articleUpvoteMilestone.ts | 2 +- src/workers/notifications/commentUpvoteMilestone.ts | 2 +- src/workers/notifications/sourcePostModerationSubmitted.ts | 2 +- src/workers/notifications/squadFeaturedUpdated.ts | 2 +- src/workers/postAddedSlackChannelSend.ts | 5 +++-- src/workers/sourceSquadCreatedUserAction.ts | 2 +- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/schema/integrations.ts b/src/schema/integrations.ts index 605d58092..224b706b2 100644 --- a/src/schema/integrations.ts +++ b/src/schema/integrations.ts @@ -355,14 +355,14 @@ export const resolvers: IResolvers = traceResolvers< .getRepository(ContentPreferenceSource) .createQueryBuilder() .select('flags') - .where('"sourceId" = :sourceId', { + .where('"referenceId" = :sourceId', { sourceId: args.sourceId, }) .andWhere(`flags->>'role' = :role`, { role: SourceMemberRoles.Admin, }) .orderBy('"createdAt"', 'ASC') - .getOne() + .getRawOne>() : null, ]); const user = await slackIntegration.user; diff --git a/src/workers/notifications/articleUpvoteMilestone.ts b/src/workers/notifications/articleUpvoteMilestone.ts index c588b2cdc..b9a3bb226 100644 --- a/src/workers/notifications/articleUpvoteMilestone.ts +++ b/src/workers/notifications/articleUpvoteMilestone.ts @@ -55,7 +55,7 @@ const worker: NotificationWorker = { .andWhere("flags->>'role' != :role", { role: SourceMemberRoles.Blocked, }) - .getMany(); + .getRawMany>(); if (!members.length) { return; diff --git a/src/workers/notifications/commentUpvoteMilestone.ts b/src/workers/notifications/commentUpvoteMilestone.ts index bd187b3fc..1b8f434ce 100644 --- a/src/workers/notifications/commentUpvoteMilestone.ts +++ b/src/workers/notifications/commentUpvoteMilestone.ts @@ -61,7 +61,7 @@ const worker: NotificationWorker = { .andWhere(`flags->>'role' != :role`, { role: SourceMemberRoles.Blocked, }) - .getOne(); + .getRawOne>(); if (!member) { return; diff --git a/src/workers/notifications/sourcePostModerationSubmitted.ts b/src/workers/notifications/sourcePostModerationSubmitted.ts index 58a3ecae2..9c46d8820 100644 --- a/src/workers/notifications/sourcePostModerationSubmitted.ts +++ b/src/workers/notifications/sourcePostModerationSubmitted.ts @@ -26,7 +26,7 @@ const worker = .andWhere(`flags->>'role' IN (:...roles)`, { roles: [SourceMemberRoles.Admin, SourceMemberRoles.Moderator], }) - .getMany(); + .getRawMany>(); const ctx: NotificationPostModerationContext = { ...moderationCtx, diff --git a/src/workers/notifications/squadFeaturedUpdated.ts b/src/workers/notifications/squadFeaturedUpdated.ts index 58c59c440..a2c8ce3ba 100644 --- a/src/workers/notifications/squadFeaturedUpdated.ts +++ b/src/workers/notifications/squadFeaturedUpdated.ts @@ -20,7 +20,7 @@ const worker = generateTypedNotificationWorker<'api.v1.squad-featured-updated'>( .select('"userId"') .where('"referenceId" = :sourceId', { sourceId: squad.id }) .andWhere(`flags->>'role' IN (:...roles)`, { roles: toNotify }) - .getMany(); + .getRawMany>(); if (!users.length) { return undefined; diff --git a/src/workers/postAddedSlackChannelSend.ts b/src/workers/postAddedSlackChannelSend.ts index ea02827f0..9f59c3329 100644 --- a/src/workers/postAddedSlackChannelSend.ts +++ b/src/workers/postAddedSlackChannelSend.ts @@ -62,15 +62,16 @@ export const postAddedSlackChannelSendWorker: TypedWorker<'api.v1.post-visible'> ); if (source.private && source.type === SourceType.Squad) { - const admin: Pick | null = await con + const admin = await con .getRepository(ContentPreferenceSource) .createQueryBuilder() + .addSelect('flags') .where('"referenceId" = :sourceId', { sourceId: source.id }) .andWhere(`flags->>'role' = :role`, { role: SourceMemberRoles.Admin, }) .orderBy('"createdAt"', 'ASC') - .getOne(); + .getRawOne>(); if (admin?.flags.referralToken) { postLink = addPrivateSourceJoinParams({ diff --git a/src/workers/sourceSquadCreatedUserAction.ts b/src/workers/sourceSquadCreatedUserAction.ts index a3177b71d..d3c46a98b 100644 --- a/src/workers/sourceSquadCreatedUserAction.ts +++ b/src/workers/sourceSquadCreatedUserAction.ts @@ -27,7 +27,7 @@ const worker: Worker = { .where('"referenceId" = :sourceId', { sourceId: source.id }) .andWhere(`flags->>'role' = :role`, { role: SourceMemberRoles.Admin }) .orderBy('"createdAt"', 'ASC') - .getOne(); + .getRawOne>(); if (!owner) { return; From e47cda097dbbc211d409518b2a1e47f7366eeb62 Mon Sep 17 00:00:00 2001 From: capJavert Date: Sat, 16 Nov 2024 01:04:48 +0100 Subject: [PATCH 3/9] feat: update tests --- __tests__/__snapshots__/feeds.ts.snap | 4 +- __tests__/boot.ts | 70 +++++++--- __tests__/comments.ts | 81 ++++++++---- __tests__/feeds.ts | 120 +++++++++++++----- __tests__/integrations.ts | 44 +++++-- __tests__/integrations/feed.ts | 74 ++++++++--- __tests__/notifications/index.ts | 20 ++- __tests__/publicSquadRequests.ts | 44 +++++-- __tests__/users.ts | 16 ++- __tests__/workers/cdc/primary.ts | 38 ++++-- .../sourcePostModerationApproved.ts | 55 +++++--- .../sourcePostModerationRejected.ts | 55 +++++--- .../sourcePostModerationSubmitted.ts | 91 +++++++++---- .../workers/postAddedSlackChannelSend.ts | 29 ++++- .../workers/sourceSquadCreatedUserAction.ts | 45 +++++-- src/workers/cdc/primary.ts | 12 +- 16 files changed, 582 insertions(+), 216 deletions(-) diff --git a/__tests__/__snapshots__/feeds.ts.snap b/__tests__/__snapshots__/feeds.ts.snap index 4c03b0c19..e8bc61274 100644 --- a/__tests__/__snapshots__/feeds.ts.snap +++ b/__tests__/__snapshots__/feeds.ts.snap @@ -37,8 +37,8 @@ Object { "excludeTypes": Array [], "includeTags": Array [], "sourceIds": Array [ - "a", - "b", + "a-sqvf", + "b-sqvf", ], } `; diff --git a/__tests__/boot.ts b/__tests__/boot.ts index 3c4cd4b7a..4e677ba75 100644 --- a/__tests__/boot.ts +++ b/__tests__/boot.ts @@ -25,7 +25,6 @@ import { Settings, SETTINGS_DEFAULT, Source, - SourceMember, SourceType, SQUAD_IMAGE_PLACEHOLDER, SquadSource, @@ -63,6 +62,8 @@ import { DEFAULT_TIMEZONE, submitArticleThreshold } from '../src/common'; import { saveReturnAlerts } from '../src/schema/alerts'; import { UserVote } from '../src/types'; import { BootAlerts, excludeProperties } from '../src/routes/boot'; +import { ContentPreferenceSource } from '../src/entity/contentPreference/ContentPreferenceSource'; +import { ContentPreferenceStatus } from '../src/entity/contentPreference/types'; let app: FastifyInstance; let con: DataSource; @@ -154,12 +155,19 @@ beforeAll(async () => { }); beforeEach(async () => { + const users = usersFixture.slice(0, 1); + jest.resetAllMocks(); jest.mocked(getEncryptedFeatures).mockReturnValue('enc'); - await con.getRepository(User).save(usersFixture[0]); + await con.getRepository(User).save(users); await con.getRepository(Source).save(sourcesFixture); await con.getRepository(Post).save(postsFixture); await ioRedisPool.execute((client) => client.flushall()); + await saveFixtures( + con, + Feed, + users.map((u) => ({ id: u.id, userId: u.id })), + ); }); const BASE_PATH = '/boot'; @@ -882,30 +890,50 @@ describe('boot misc', () => { active: false, }, ]); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { sourceId: 's1', + referenceId: 's1', userId: '1', - referralToken: 'rt', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { sourceId: 's2', + referenceId: 's2', userId: '1', - referralToken: 'rt2', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { sourceId: 's4', + referenceId: 's4', userId: '1', - referralToken: 'rt3', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt3', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { sourceId: 's5', + referenceId: 's5', userId: '1', - referralToken: 'rt5', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt5', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, ]); const res = await request(app.server) @@ -985,18 +1013,28 @@ describe('boot misc', () => { active: false, }, ]); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { sourceId: 's1', + referenceId: 's1', userId: '1', - referralToken: 'rt', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { sourceId: 's3', + referenceId: 's3', userId: '1', - referralToken: 'rt3', - role: SourceMemberRoles.Blocked, + flags: { + role: SourceMemberRoles.Blocked, + referralToken: 'rt3', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, ]); const res = await request(app.server) diff --git a/__tests__/comments.ts b/__tests__/comments.ts index ea93b9fc1..dce9405d2 100644 --- a/__tests__/comments.ts +++ b/__tests__/comments.ts @@ -15,9 +15,9 @@ import { Comment, User, ArticlePost, - SourceMember, SourceType, CommentMention, + Feed, } from '../src/entity'; import { SourceMemberRoles } from '../src/roles'; import { sourcesFixture } from './fixture/source'; @@ -44,6 +44,8 @@ import { getRedisObjectExpiry, } from '../src/redis'; import { badUsersFixture } from './fixture'; +import { ContentPreferenceStatus } from '../src/entity/contentPreference/types'; +import { ContentPreferenceSource } from '../src/entity/contentPreference/ContentPreferenceSource'; let con: DataSource; let state: GraphQLTestingState; @@ -77,6 +79,11 @@ beforeEach(async () => { await saveFixtures(con, ArticlePost, sharedPostsFixture); await saveFixtures(con, PostTag, postTagsFixture); await con.getRepository(User).save(usersFixture); + await saveFixtures( + con, + Feed, + usersFixture.map((item) => ({ id: item.id, userId: item.id })), + ); await con.getRepository(User).save(badUsersFixture); await con.getRepository(Comment).save([ { @@ -190,20 +197,30 @@ const saveSquadFixture = async (sourceId: string) => { }, ]); await con.getRepository(Source).update({ id: sourceId }, { private: true }); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { - userId: '1', sourceId, - role: SourceMemberRoles.Admin, - referralToken: 'rt1', + referenceId: sourceId, + userId: '1', createdAt: new Date(2022, 11, 19), + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { - userId: 'sample1', sourceId, - role: SourceMemberRoles.Member, - referralToken: 'rt2', + referenceId: sourceId, + userId: 'sample1', createdAt: new Date(2022, 11, 19), + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, ]); }; @@ -913,11 +930,16 @@ describe('mutation commentOnPost', () => { await con .getRepository(Post) .update({ id: 'p1' }, { private: false, sourceId: 'a' }); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', + referenceId: 'a', userId: '1', - referralToken: 'rt2', - role: SourceMemberRoles.Blocked, + flags: { + role: SourceMemberRoles.Blocked, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); return testMutationErrorCode( client, @@ -1152,11 +1174,16 @@ describe('mutation commentOnComment', () => { await con .getRepository(Post) .update({ id: 'p1' }, { private: false, sourceId: 'a' }); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', + referenceId: 'a', userId: '1', - referralToken: 'rt2', - role: SourceMemberRoles.Blocked, + flags: { + role: SourceMemberRoles.Blocked, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); return testMutationErrorCode( @@ -1349,11 +1376,16 @@ describe('mutation deleteComment', () => { it("should forbidden when other user doesn't have the right permissions", async () => { loggedUser = '1'; - await con.getRepository(SourceMember).insert({ - userId: '1', + await con.getRepository(ContentPreferenceSource).insert({ sourceId: 'squad', - role: SourceMemberRoles.Member, - referralToken: 's1', + referenceId: 'squad', + userId: '1', + flags: { + role: SourceMemberRoles.Member, + referralToken: 's1', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); return testMutationErrorCode( client, @@ -1367,11 +1399,16 @@ describe('mutation deleteComment', () => { it('should delete a comment if source admin has permissions', async () => { loggedUser = '1'; - await con.getRepository(SourceMember).insert({ - userId: '1', + await con.getRepository(ContentPreferenceSource).insert({ sourceId: 'squad', - role: SourceMemberRoles.Admin, - referralToken: 's1', + referenceId: 'squad', + userId: '1', + flags: { + role: SourceMemberRoles.Admin, + referralToken: 's1', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); const before = await con.getRepository(Comment).findBy({ postId: 'p1' }); expect(before.length).toEqual(5); diff --git a/__tests__/feeds.ts b/__tests__/feeds.ts index cc9e7ad02..33a3246c6 100644 --- a/__tests__/feeds.ts +++ b/__tests__/feeds.ts @@ -20,7 +20,6 @@ import { PostType, SharePost, Source, - SourceMember, SourceType, User, UserPost, @@ -1248,13 +1247,19 @@ describe('query sourceFeed', () => { loggedUser = '1'; await con.getRepository(Source).update({ id: 'b' }, { private: true }); await con.getRepository(User).save(usersFixture[0]); - await con.getRepository(SourceMember).save([ + await saveFixtures(con, Feed, [{ id: '1', userId: '1' }]); + await con.getRepository(ContentPreferenceSource).save([ { - userId: '1', sourceId: 'b', - role: SourceMemberRoles.Admin, - referralToken: randomUUID(), + referenceId: 'b', + userId: '1', createdAt: new Date(2022, 11, 19), + flags: { + role: SourceMemberRoles.Admin, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, ]); const res = await client.query(QUERY('b')); @@ -1267,6 +1272,7 @@ describe('query sourceFeed', () => { loggedUser = '1'; additionalProps = 'type image'; await con.getRepository(User).save(usersFixture[0]); + await saveFixtures(con, Feed, [{ id: '1', userId: '1' }]); const repo = con.getRepository(Source); await repo.update({ id: 'b' }, { private: true }); const source = await repo.findOneBy({ id: 'b' }); @@ -1281,13 +1287,18 @@ describe('query sourceFeed', () => { await con .getRepository(WelcomePost) .update({ id: welcome.id }, { image: null }); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { - userId: '1', sourceId: 'b', - role: SourceMemberRoles.Admin, - referralToken: randomUUID(), + referenceId: 'b', + userId: '1', createdAt: new Date(2022, 11, 19), + flags: { + role: SourceMemberRoles.Admin, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, ]); @@ -1307,14 +1318,20 @@ describe('query sourceFeed', () => { it('should disallow access to feed for public source for blocked members', async () => { loggedUser = '1'; await con.getRepository(User).save(usersFixture[0]); + await saveFixtures(con, Feed, [{ id: '1', userId: '1' }]); await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad, private: false }); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', + referenceId: 'a', userId: '1', - referralToken: 'rt2', - role: SourceMemberRoles.Blocked, + flags: { + role: SourceMemberRoles.Blocked, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); return testQueryErrorCode( @@ -2821,18 +2838,39 @@ describe('function feedToFilters', () => { it('should return filters with source memberships', async () => { loggedUser = '1'; await saveFixtures(con, User, [usersFixture[0]]); - await con.getRepository(SourceMember).save([ + await saveFixtures( + con, + Source, + sourcesFixture.map((item) => ({ + ...item, + id: `${item.id}-sqvf`, + handle: `${item.handle}-sqvf`, + type: SourceType.Squad, + })), + ); + await saveFixtures(con, Feed, [{ id: '1', userId: '1' }]); + await con.getRepository(ContentPreferenceSource).save([ { + sourceId: 'a-sqvf', + referenceId: 'a-sqvf', userId: '1', - sourceId: 'a', - role: SourceMemberRoles.Member, - referralToken: 'rt', + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { + sourceId: 'b-sqvf', + referenceId: 'b-sqvf', userId: '1', - sourceId: 'b', - role: SourceMemberRoles.Admin, - referralToken: 'rt2', + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, ]); expect(await feedToFilters(con, '1', '1')).toMatchSnapshot(); @@ -2885,14 +2923,19 @@ describe('function feedToFilters', () => { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad, private: true }); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { - userId: '1', sourceId: 'a', - role: SourceMemberRoles.Member, - referralToken: 'rt2', + referenceId: 'a', + userId: '1', createdAt: new Date(2022, 11, 19), - flags: { hideFeedPosts: true }, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt2', + hideFeedPosts: true, + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, ]); const filters = await feedToFilters(con, '1', '1'); @@ -2907,14 +2950,19 @@ describe('function feedToFilters', () => { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad, private: true }); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { - userId: '1', sourceId: 'a', - role: SourceMemberRoles.Member, - referralToken: 'rt2', + referenceId: 'a', + userId: '1', createdAt: new Date(2022, 11, 19), - flags: { hideFeedPosts: false }, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt2', + hideFeedPosts: false, + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, ]); const filters = await feedToFilters(con, '1', '1'); @@ -2928,14 +2976,18 @@ describe('function feedToFilters', () => { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad, private: true }); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { - userId: '1', sourceId: 'a', - role: SourceMemberRoles.Member, - referralToken: 'rt2', + referenceId: 'a', + userId: '1', createdAt: new Date(2022, 11, 19), - flags: {}, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, ]); const filters = await feedToFilters(con, '1', '1'); diff --git a/__tests__/integrations.ts b/__tests__/integrations.ts index 99ba47129..dcb4d4c57 100644 --- a/__tests__/integrations.ts +++ b/__tests__/integrations.ts @@ -15,13 +15,7 @@ import { UserIntegrationType, } from '../src/entity/UserIntegration'; import { usersFixture } from './fixture/user'; -import { - Source, - SourceMember, - SourceType, - SquadSource, - User, -} from '../src/entity'; +import { Source, SquadSource, User, SourceType, Feed } from '../src/entity'; import { Context } from '../src/Context'; import { sourcesFixture } from './fixture/source'; import { encrypt } from '../src/common'; @@ -31,6 +25,8 @@ import { } from '../src/entity/UserSourceIntegration'; import { SourceMemberRoles } from '../src/roles'; import { addSeconds } from 'date-fns'; +import { ContentPreferenceStatus } from '../src/entity/contentPreference/types'; +import { ContentPreferenceSource } from '../src/entity/contentPreference/ContentPreferenceSource'; const slackPostMessage = jest.fn().mockResolvedValue({ ok: true, @@ -116,6 +112,11 @@ describe('slack integration', () => { const createdAt = new Date(); await saveFixtures(con, User, usersFixture); + await saveFixtures( + con, + Feed, + usersFixture.map((user) => ({ id: user.id, userId: user.id })), + ); await saveFixtures(con, Source, sourcesFixture); await con.getRepository(UserIntegration).save([ { @@ -165,27 +166,42 @@ describe('slack integration', () => { private: true, }, ]); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { sourceId: 'squadslack', + referenceId: 'squadslack', userId: '1', - role: SourceMemberRoles.Admin, - referralToken: 'squadslacktoken1', createdAt: addSeconds(createdAt, 1), + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'squadslacktoken1', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { sourceId: 'squadslack', + referenceId: 'squadslack', userId: '2', - role: SourceMemberRoles.Admin, - referralToken: 'squadslacktoken2', createdAt: addSeconds(createdAt, 2), + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'squadslacktoken2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '2', }, { sourceId: 'squadslack', + referenceId: 'squadslack', userId: '3', - role: SourceMemberRoles.Member, - referralToken: 'squadslacktoken3', createdAt: addSeconds(createdAt, 3), + flags: { + role: SourceMemberRoles.Member, + referralToken: 'squadslacktoken3', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '3', }, ]); }); diff --git a/__tests__/integrations/feed.ts b/__tests__/integrations/feed.ts index ac84feda9..dc70384a0 100644 --- a/__tests__/integrations/feed.ts +++ b/__tests__/integrations/feed.ts @@ -22,7 +22,7 @@ import { PostType, postTypes, Source, - SourceMember, + SourceType, User, } from '../../src/entity'; import { SourceMemberRoles } from '../../src/roles'; @@ -140,6 +140,16 @@ describe('FeedClient', () => { describe('FeedPreferencesConfigGenerator', () => { beforeEach(async () => { await saveFixtures(con, Source, sourcesFixture); + await saveFixtures( + con, + Source, + sourcesFixture.map((item) => ({ + ...item, + id: `${item.id}-lofnpr`, + handle: `${item.handle}-lofnpr`, + type: SourceType.Squad, + })), + ); await saveFixtures(con, Keyword, [ { value: 'javascript', @@ -204,19 +214,27 @@ describe('FeedPreferencesConfigGenerator', () => { status: ContentPreferenceStatus.Blocked, referenceId: 'b', }, - ]); - await con.getRepository(SourceMember).save([ { + sourceId: 'a-lofnpr', + referenceId: 'a-lofnpr', userId: '1', - sourceId: 'a', - role: SourceMemberRoles.Member, - referralToken: 'rt', + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { + sourceId: 'b-lofnpr', + referenceId: 'b-lofnpr', userId: '1', - sourceId: 'b', - role: SourceMemberRoles.Admin, - referralToken: 'rt2', + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, ]); await con.getRepository(AdvancedSettings).save([ @@ -277,7 +295,7 @@ describe('FeedPreferencesConfigGenerator', () => { fresh_page_size: '1', offset: 3, page_size: 2, - squad_ids: expect.arrayContaining(['a', 'b']), + squad_ids: expect.arrayContaining(['a-lofnpr', 'b-lofnpr']), total_pages: 20, user_id: '1', }, @@ -507,6 +525,16 @@ describe('FeedUserStateConfigGenerator', () => { describe('FeedLofnConfigGenerator', () => { beforeEach(async () => { await saveFixtures(con, Source, sourcesFixture); + await saveFixtures( + con, + Source, + sourcesFixture.map((item) => ({ + ...item, + id: `${item.id}-lofncg`, + handle: `${item.handle}-lofncg`, + type: SourceType.Squad, + })), + ); await con.getRepository(Feed).save({ id: '1', userId: 'u1' }); await saveFixtures(con, Keyword, [ { @@ -571,19 +599,27 @@ describe('FeedLofnConfigGenerator', () => { status: ContentPreferenceStatus.Blocked, referenceId: 'b', }, - ]); - await con.getRepository(SourceMember).save([ { + sourceId: 'a-lofncg', + referenceId: 'a-lofncg', userId: '1', - sourceId: 'a', - role: SourceMemberRoles.Member, - referralToken: 'rt', + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { + sourceId: 'b-lofncg', + referenceId: 'b-lofncg', userId: '1', - sourceId: 'b', - role: SourceMemberRoles.Admin, - referralToken: 'rt2', + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, ]); await con.getRepository(AdvancedSettings).save([ @@ -659,7 +695,7 @@ describe('FeedLofnConfigGenerator', () => { allowed_tags: expect.arrayContaining(['javascript', 'golang']), blocked_tags: expect.arrayContaining(['python', 'java']), blocked_sources: expect.arrayContaining(['a', 'b']), - squad_ids: expect.arrayContaining(['a', 'b']), + squad_ids: expect.arrayContaining(['a-lofncg', 'b-lofncg']), allowed_post_types: expect.arrayContaining([ 'article', 'share', diff --git a/__tests__/notifications/index.ts b/__tests__/notifications/index.ts index bfcfd71b8..ec752bfa1 100644 --- a/__tests__/notifications/index.ts +++ b/__tests__/notifications/index.ts @@ -22,6 +22,7 @@ import { postsFixture } from '../fixture/post'; import { Bookmark, Comment, + Feed, FreeformPost, Keyword, NotificationAttachmentType, @@ -32,7 +33,6 @@ import { PostType, SharePost, Source, - SourceMember, SourceRequest, SourceType, SquadSource, @@ -57,6 +57,8 @@ import { SourceMemberRoles } from '../../src/roles'; import { NotificationType } from '../../src/notifications/common'; import { format } from 'date-fns'; import { saveFixtures } from '../helpers'; +import { ContentPreferenceStatus } from '../../src/entity/contentPreference/types'; +import { ContentPreferenceSource } from '../../src/entity/contentPreference/ContentPreferenceSource'; const userId = '1'; const commentFixture: Reference = { @@ -848,11 +850,21 @@ describe('generateNotification', () => { .update({ id: 'a' }, { type: SourceType.Squad }); const source = await con.getRepository(Source).findOneBy({ id: 'a' }); await con.getRepository(User).save(usersFixture); - await con.getRepository(SourceMember).save({ + await saveFixtures( + con, + Feed, + usersFixture.map((item) => ({ id: item.id, userId: item.id })), + ); + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', + referenceId: 'a', userId: '1', - role: SourceMemberRoles.Admin, - referralToken: 'random', + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'random', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); const post = await createSquadWelcomePost(con, source as SquadSource, '1'); await con diff --git a/__tests__/publicSquadRequests.ts b/__tests__/publicSquadRequests.ts index 9e15bda32..c84b2f186 100644 --- a/__tests__/publicSquadRequests.ts +++ b/__tests__/publicSquadRequests.ts @@ -11,8 +11,8 @@ import { } from './helpers'; import { Roles, SourceMemberRoles } from '../src/roles'; import { + Feed, Source, - SourceMember, SquadPublicRequest, SquadPublicRequestStatus, User, @@ -21,6 +21,8 @@ import { sourcesFixture } from './fixture/source'; import { usersFixture } from './fixture/user'; import { DataSource } from 'typeorm'; import createOrGetConnection from '../src/db'; +import { ContentPreferenceStatus } from '../src/entity/contentPreference/types'; +import { ContentPreferenceSource } from '../src/entity/contentPreference/ContentPreferenceSource'; let con: DataSource; let state: GraphQLTestingState; @@ -52,27 +54,47 @@ beforeEach(async () => { roles = []; await saveFixtures(con, Source, [sourcesFixture[5]]); await saveFixtures(con, User, usersFixture); - await con.getRepository(SourceMember).save([ + await saveFixtures( + con, + Feed, + usersFixture.map((u) => ({ id: u.id, userId: u.id })), + ); + await con.getRepository(ContentPreferenceSource).save([ { - userId: '1', sourceId, - role: SourceMemberRoles.Admin, - referralToken: 'rt1', + referenceId: sourceId, + userId: '1', createdAt: new Date(2022, 11, 19), + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'rt1', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { - userId: '2', sourceId, - role: SourceMemberRoles.Moderator, - referralToken: 'rt2', + referenceId: sourceId, + userId: '2', createdAt: new Date(2022, 11, 20), + flags: { + role: SourceMemberRoles.Moderator, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '2', }, { - userId: '3', sourceId, - role: SourceMemberRoles.Member, - referralToken: 'rt3', + referenceId: sourceId, + userId: '3', createdAt: new Date(2022, 11, 19), + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt3', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '3', }, ]); diff --git a/__tests__/users.ts b/__tests__/users.ts index b14a66e76..1b3bd0ef4 100644 --- a/__tests__/users.ts +++ b/__tests__/users.ts @@ -43,7 +43,6 @@ import { reputationReasonAmount, ReputationType, Source, - SourceMember, User, UserMarketingCta, UserPersonalizedDigest, @@ -109,6 +108,7 @@ import { ContentPreferenceUser } from '../src/entity/contentPreference/ContentPr import { ContentPreferenceStatus } from '../src/entity/contentPreference/types'; import { identifyUserPersonalizedDigest } from '../src/cio'; import type { GQLUser } from '../src/schema/users'; +import { ContentPreferenceSource } from '../src/entity/contentPreference/ContentPreferenceSource'; let con: DataSource; let app: FastifyInstance; @@ -5382,11 +5382,17 @@ describe('mutation sendReport', () => { it('should report private source as a member', async () => { loggedUser = '1'; await con.getRepository(Source).update({ id: 'a' }, { private: true }); - await con.getRepository(SourceMember).save({ - userId: loggedUser, + await saveFixtures(con, Feed, [{ id: loggedUser, userId: loggedUser }]); + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', - role: SourceMemberRoles.Member, - referralToken: 't1', + referenceId: 'a', + userId: loggedUser, + flags: { + role: SourceMemberRoles.Member, + referralToken: 't1', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: loggedUser, }); const res = await client.mutate(MUTATION, { variables }); expect(res.errors).toBeFalsy(); diff --git a/__tests__/workers/cdc/primary.ts b/__tests__/workers/cdc/primary.ts index e3cf43a05..dcfe0ca50 100644 --- a/__tests__/workers/cdc/primary.ts +++ b/__tests__/workers/cdc/primary.ts @@ -95,7 +95,6 @@ import { Settings, Source, SourceFeed, - SourceMember, SourceRequest, Submission, SubmissionStatus, @@ -145,6 +144,8 @@ import { SourcePostModeration, SourcePostModerationStatus, } from '../../../src/entity/SourcePostModeration'; +import { ContentPreferenceSource } from '../../../src/entity/contentPreference/ContentPreferenceSource'; +import { ContentPreferenceStatus } from '../../../src/entity/contentPreference/types'; jest.mock('../../../src/common', () => ({ ...(jest.requireActual('../../../src/common') as Record), @@ -1961,14 +1962,30 @@ describe('submission', () => { }); describe('source member', () => { - type ObjectType = Partial; + type ObjectType = Partial; const base: ChangeObject = { + sourceId: 'a-cdcsm', + referenceId: 'a-cdcsm', userId: '1', - sourceId: 'a', - referralToken: 'rt', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }; + beforeEach(async () => { + await saveFixtures(con, Source, [ + { + ...sourcesFixture[0], + id: `${sourcesFixture[0].id}-cdcsm`, + handle: `${sourcesFixture[0].handle}-cdcsm`, + type: SourceType.Squad, + }, + ]); + }); + it('should notify on new source member', async () => { await expectSuccessfulBackground( worker, @@ -1976,7 +1993,7 @@ describe('source member', () => { after: base, before: null, op: 'c', - table: 'source_member', + table: 'content_preference', }), ); expect(notifyMemberJoinedSource).toHaveBeenCalledTimes(1); @@ -1988,7 +2005,10 @@ describe('source member', () => { it('should notify when role changed', async () => { const after: ChangeObject = { ...base, - role: SourceMemberRoles.Admin, + flags: { + ...base.flags, + role: SourceMemberRoles.Admin, + }, }; await saveFixtures(con, User, [defaultUser]); await expectSuccessfulBackground( @@ -1997,13 +2017,13 @@ describe('source member', () => { after, before: base, op: 'u', - table: 'source_member', + table: 'content_preference', }), ); expect(notifySourceMemberRoleChanged).toHaveBeenCalledTimes(1); expect( jest.mocked(notifySourceMemberRoleChanged).mock.calls[0].slice(1), - ).toEqual([base.role, after]); + ).toEqual([base.flags!.role, after]); }); it("should not notify if role doesn't change", async () => { diff --git a/__tests__/workers/notifications/sourcePostModerationApproved.ts b/__tests__/workers/notifications/sourcePostModerationApproved.ts index 285976d0b..ff90ca5df 100644 --- a/__tests__/workers/notifications/sourcePostModerationApproved.ts +++ b/__tests__/workers/notifications/sourcePostModerationApproved.ts @@ -1,13 +1,7 @@ import { DataSource } from 'typeorm'; import worker from '../../../src/workers/notifications/sourcePostModerationApproved'; import createOrGetConnection from '../../../src/db'; -import { - Post, - Source, - SourceMember, - SourceType, - User, -} from '../../../src/entity'; +import { Feed, Post, Source, SourceType, User } from '../../../src/entity'; import { sourcesFixture, usersFixture } from '../../fixture'; import { workers } from '../../../src/workers'; import { invokeNotificationWorker, saveFixtures } from '../../helpers'; @@ -15,6 +9,8 @@ import { SourcePostModerationStatus } from '../../../src/entity/SourcePostModera import { SourceMemberRoles } from '../../../src/roles'; import { NotificationPostModerationContext } from '../../../src/notifications'; import { postsFixture } from '../../fixture/post'; +import { ContentPreferenceStatus } from '../../../src/entity/contentPreference/types'; +import { ContentPreferenceSource } from '../../../src/entity/contentPreference/ContentPreferenceSource'; let con: DataSource; @@ -26,6 +22,11 @@ beforeEach(async () => { jest.resetAllMocks(); await saveFixtures(con, Source, sourcesFixture); await saveFixtures(con, User, usersFixture); + await saveFixtures( + con, + Feed, + usersFixture.map((u) => ({ id: u.id, userId: u.id })), + ); await saveFixtures(con, Post, postsFixture); }); @@ -74,11 +75,16 @@ describe('SourcePostModerationSubmitted', () => { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', + referenceId: 'a', userId: '1', - role: SourceMemberRoles.Admin, - referralToken: 'a', + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'a', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); const result = await invokeNotificationWorker(worker, { post }); @@ -100,24 +106,39 @@ describe('SourcePostModerationSubmitted', () => { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { sourceId: 'a', + referenceId: 'a', userId: '1', - role: SourceMemberRoles.Moderator, - referralToken: 'a', + flags: { + role: SourceMemberRoles.Moderator, + referralToken: 'a', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { sourceId: 'a', + referenceId: 'a', userId: '3', - role: SourceMemberRoles.Member, - referralToken: 'b', + flags: { + role: SourceMemberRoles.Member, + referralToken: 'b', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '3', }, { sourceId: 'a', + referenceId: 'a', userId: '4', - role: SourceMemberRoles.Moderator, - referralToken: 'c', + flags: { + role: SourceMemberRoles.Moderator, + referralToken: 'c', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '4', }, ]); diff --git a/__tests__/workers/notifications/sourcePostModerationRejected.ts b/__tests__/workers/notifications/sourcePostModerationRejected.ts index 4008a6e1f..0ee6073a9 100644 --- a/__tests__/workers/notifications/sourcePostModerationRejected.ts +++ b/__tests__/workers/notifications/sourcePostModerationRejected.ts @@ -1,13 +1,7 @@ import { DataSource } from 'typeorm'; import worker from '../../../src/workers/notifications/sourcePostModerationRejected'; import createOrGetConnection from '../../../src/db'; -import { - Post, - Source, - SourceMember, - SourceType, - User, -} from '../../../src/entity'; +import { Feed, Post, Source, SourceType, User } from '../../../src/entity'; import { sourcesFixture, usersFixture } from '../../fixture'; import { workers } from '../../../src/workers'; import { invokeNotificationWorker, saveFixtures } from '../../helpers'; @@ -15,6 +9,8 @@ import { SourcePostModerationStatus } from '../../../src/entity/SourcePostModera import { SourceMemberRoles } from '../../../src/roles'; import { NotificationPostModerationContext } from '../../../src/notifications'; import { postsFixture } from '../../fixture/post'; +import { ContentPreferenceStatus } from '../../../src/entity/contentPreference/types'; +import { ContentPreferenceSource } from '../../../src/entity/contentPreference/ContentPreferenceSource'; let con: DataSource; @@ -26,6 +22,11 @@ beforeEach(async () => { jest.resetAllMocks(); await saveFixtures(con, Source, sourcesFixture); await saveFixtures(con, User, usersFixture); + await saveFixtures( + con, + Feed, + usersFixture.map((u) => ({ id: u.id, userId: u.id })), + ); await saveFixtures(con, Post, postsFixture); }); @@ -71,11 +72,16 @@ describe('SourcePostModerationSubmitted', () => { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', + referenceId: 'a', userId: '1', - role: SourceMemberRoles.Admin, - referralToken: 'a', + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'a', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); const result = await invokeNotificationWorker(worker, { post }); @@ -95,24 +101,39 @@ describe('SourcePostModerationSubmitted', () => { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { sourceId: 'a', + referenceId: 'a', userId: '1', - role: SourceMemberRoles.Moderator, - referralToken: 'a', + flags: { + role: SourceMemberRoles.Moderator, + referralToken: 'a', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { sourceId: 'a', + referenceId: 'a', userId: '3', - role: SourceMemberRoles.Member, - referralToken: 'b', + flags: { + role: SourceMemberRoles.Member, + referralToken: 'b', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '3', }, { sourceId: 'a', + referenceId: 'a', userId: '4', - role: SourceMemberRoles.Moderator, - referralToken: 'c', + flags: { + role: SourceMemberRoles.Moderator, + referralToken: 'c', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '4', }, ]); diff --git a/__tests__/workers/notifications/sourcePostModerationSubmitted.ts b/__tests__/workers/notifications/sourcePostModerationSubmitted.ts index 312d30326..3399994e3 100644 --- a/__tests__/workers/notifications/sourcePostModerationSubmitted.ts +++ b/__tests__/workers/notifications/sourcePostModerationSubmitted.ts @@ -1,13 +1,15 @@ import { DataSource } from 'typeorm'; import worker from '../../../src/workers/notifications/sourcePostModerationSubmitted'; import createOrGetConnection from '../../../src/db'; -import { Source, SourceMember, SourceType, User } from '../../../src/entity'; +import { Feed, Source, SourceType, User } from '../../../src/entity'; import { sourcesFixture, usersFixture } from '../../fixture'; import { workers } from '../../../src/workers'; import { invokeNotificationWorker, saveFixtures } from '../../helpers'; import { SourcePostModerationStatus } from '../../../src/entity/SourcePostModeration'; import { SourceMemberRoles } from '../../../src/roles'; import { NotificationPostModerationContext } from '../../../src/notifications'; +import { ContentPreferenceSource } from '../../../src/entity/contentPreference/ContentPreferenceSource'; +import { ContentPreferenceStatus } from '../../../src/entity/contentPreference/types'; let con: DataSource; @@ -19,6 +21,11 @@ beforeEach(async () => { jest.resetAllMocks(); await saveFixtures(con, Source, sourcesFixture); await saveFixtures(con, User, usersFixture); + await saveFixtures( + con, + Feed, + usersFixture.map((u) => ({ id: u.id, userId: u.id })), + ); }); describe('SourcePostModerationSubmitted', () => { @@ -50,11 +57,16 @@ describe('SourcePostModerationSubmitted', () => { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', + referenceId: 'a', userId: '1', - role: SourceMemberRoles.Admin, - referralToken: 'a', + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'a', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); const result = await invokeNotificationWorker(worker, { post }); @@ -75,11 +87,16 @@ describe('SourcePostModerationSubmitted', () => { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', + referenceId: 'a', userId: '1', - role: SourceMemberRoles.Moderator, - referralToken: 'a', + flags: { + role: SourceMemberRoles.Moderator, + referralToken: 'a', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); const result = await invokeNotificationWorker(worker, { post }); @@ -100,18 +117,28 @@ describe('SourcePostModerationSubmitted', () => { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { sourceId: 'a', + referenceId: 'a', userId: '1', - role: SourceMemberRoles.Moderator, - referralToken: 'a', + flags: { + role: SourceMemberRoles.Moderator, + referralToken: 'a', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { sourceId: 'a', + referenceId: 'a', userId: '3', - role: SourceMemberRoles.Member, - referralToken: 'b', + flags: { + role: SourceMemberRoles.Member, + referralToken: 'b', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '3', }, ]); @@ -135,18 +162,28 @@ describe('SourcePostModerationSubmitted', () => { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { sourceId: 'a', + referenceId: 'a', userId: '1', - role: SourceMemberRoles.Moderator, - referralToken: 'a', + flags: { + role: SourceMemberRoles.Moderator, + referralToken: 'a', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { sourceId: 'a', + referenceId: 'a', userId: '3', - role: SourceMemberRoles.Blocked, - referralToken: 'b', + flags: { + role: SourceMemberRoles.Blocked, + referralToken: 'b', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '3', }, ]); @@ -170,18 +207,28 @@ describe('SourcePostModerationSubmitted', () => { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { sourceId: 'a', + referenceId: 'a', userId: '1', - role: SourceMemberRoles.Moderator, - referralToken: 'a', + flags: { + role: SourceMemberRoles.Moderator, + referralToken: 'a', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { sourceId: 'a', + referenceId: 'a', userId: '3', - role: SourceMemberRoles.Blocked, - referralToken: 'b', + flags: { + role: SourceMemberRoles.Blocked, + referralToken: 'b', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '3', }, ]); diff --git a/__tests__/workers/postAddedSlackChannelSend.ts b/__tests__/workers/postAddedSlackChannelSend.ts index 330aa7a33..008f4854d 100644 --- a/__tests__/workers/postAddedSlackChannelSend.ts +++ b/__tests__/workers/postAddedSlackChannelSend.ts @@ -2,9 +2,9 @@ import { expectSuccessfulTypedBackground, saveFixtures } from '../helpers'; import { postAddedSlackChannelSendWorker as worker } from '../../src/workers/postAddedSlackChannelSend'; import { ArticlePost, + Feed, Post, Source, - SourceMember, SourceType, SquadSource, User, @@ -25,6 +25,8 @@ import { ChangeObject } from '../../src/types'; import { SourceMemberRoles } from '../../src/roles'; import { addSeconds } from 'date-fns'; import { SlackApiErrorCode } from '../../src/errors'; +import { ContentPreferenceStatus } from '../../src/entity/contentPreference/types'; +import { ContentPreferenceSource } from '../../src/entity/contentPreference/ContentPreferenceSource'; const conversationsJoin = jest.fn().mockResolvedValue({ ok: true, @@ -61,6 +63,11 @@ describe('postAddedSlackChannelSend worker', () => { await saveFixtures(con, Source, sourcesFixture); await saveFixtures(con, ArticlePost, postsFixture); await saveFixtures(con, User, usersFixture); + await saveFixtures( + con, + Feed, + usersFixture.map((u) => ({ id: u.id, userId: u.id })), + ); await con.getRepository(SquadSource).save([ { id: 'squadslackchannel', @@ -73,20 +80,30 @@ describe('postAddedSlackChannelSend worker', () => { }, ]); const createdAt = new Date(); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { sourceId: 'squadslackchannel', + referenceId: 'squadslackchannel', userId: '1', - role: SourceMemberRoles.Admin, - referralToken: 'squadslackchanneltoken2', createdAt: addSeconds(createdAt, 2), + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'squadslackchanneltoken2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { sourceId: 'squadslackchannel', + referenceId: 'squadslackchannel', userId: '2', - role: SourceMemberRoles.Admin, - referralToken: 'squadslackchanneltoken1', createdAt: addSeconds(createdAt, 1), + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'squadslackchanneltoken1', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '2', }, ]); const [userIntegration] = await con.getRepository(UserIntegration).save([ diff --git a/__tests__/workers/sourceSquadCreatedUserAction.ts b/__tests__/workers/sourceSquadCreatedUserAction.ts index f7c3a44ab..bab586fdc 100644 --- a/__tests__/workers/sourceSquadCreatedUserAction.ts +++ b/__tests__/workers/sourceSquadCreatedUserAction.ts @@ -5,15 +5,17 @@ import createOrGetConnection from '../../src/db'; import { SourceType, SquadSource, - SourceMember, User, Source, UserAction, UserActionType, + Feed, } from '../../src/entity'; import { SourceMemberRoles } from '../../src/roles'; import { usersFixture } from '../fixture/user'; import { createSource } from '../fixture/source'; +import { ContentPreferenceSource } from '../../src/entity/contentPreference/ContentPreferenceSource'; +import { ContentPreferenceStatus } from '../../src/entity/contentPreference/types'; let con: DataSource; @@ -25,6 +27,11 @@ beforeEach(async () => { jest.resetAllMocks(); await saveFixtures(con, User, usersFixture); + await saveFixtures( + con, + Feed, + usersFixture.map((u) => ({ id: u.id, userId: u.id })), + ); await con .getRepository(SquadSource) .save([ @@ -35,18 +42,30 @@ beforeEach(async () => { SourceType.Squad, ), ]); - await con.getRepository(SourceMember).save({ - sourceId: 'squadCreatedUA_s1', - userId: '2', - referralToken: 'sourceSquadCreatedUserAction_rt1', - role: SourceMemberRoles.Admin, - }); - await con.getRepository(SourceMember).save({ - sourceId: 'squadCreatedUA_s1', - userId: '1', - referralToken: 'sourceSquadCreatedUserAction_rt2', - role: SourceMemberRoles.Admin, - }); + await con.getRepository(ContentPreferenceSource).save([ + { + sourceId: 'squadCreatedUA_s1', + referenceId: 'squadCreatedUA_s1', + userId: '2', + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'sourceSquadCreatedUserAction_rt1', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '2', + }, + { + sourceId: 'squadCreatedUA_s1', + referenceId: 'squadCreatedUA_s1', + userId: '1', + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'sourceSquadCreatedUserAction_rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', + }, + ]); }); describe('sourceSquadCreatedUserAction worker', () => { diff --git a/src/workers/cdc/primary.ts b/src/workers/cdc/primary.ts index fbf1dfda6..d5935579d 100644 --- a/src/workers/cdc/primary.ts +++ b/src/workers/cdc/primary.ts @@ -842,12 +842,14 @@ const onSourceMemberChange = async ( const sourceId = data.payload.after!.sourceId || data.payload.before!.sourceId; - const source = await con.getRepository(Source).findOne({ - select: ['id'], - where: { + const source = await con + .getRepository(Source) + .createQueryBuilder() + .select('type') + .where({ id: sourceId, - }, - }); + }) + .getRawOne>(); if (source?.type !== SourceType.Squad) { return; From d045cdf062ef9e0860fdee8c71037a2e7f66b816 Mon Sep 17 00:00:00 2001 From: capJavert Date: Mon, 18 Nov 2024 13:14:58 +0100 Subject: [PATCH 4/9] feat: notifications tests --- __tests__/workers/notifications.ts | 372 +++++++++++++----- src/workers/notifications/postAdded.ts | 2 +- .../notifications/squadMemberJoined.ts | 2 +- src/workers/notifications/utils.ts | 9 +- 4 files changed, 285 insertions(+), 100 deletions(-) diff --git a/__tests__/workers/notifications.ts b/__tests__/workers/notifications.ts index 1549bdf35..aada3e1ae 100644 --- a/__tests__/workers/notifications.ts +++ b/__tests__/workers/notifications.ts @@ -14,7 +14,6 @@ import { PostType, Settings, Source, - SourceMember, SourceType, SquadSource, SubmissionStatus, @@ -45,16 +44,24 @@ import { NotificationPreferenceStatus, NotificationType, } from '../../src/notifications/common'; -import { createSquadWelcomePost, NotificationReason } from '../../src/common'; +import { + createSquadWelcomePost, + NotificationReason, + updateFlagsStatement, +} from '../../src/common'; import { randomUUID } from 'crypto'; -import { UserVote } from '../../src/types'; +import { ChangeObject, UserVote } from '../../src/types'; import { UserComment } from '../../src/entity/user/UserComment'; import { workers } from '../../src/workers'; import { generateStorageKey, StorageKey, StorageTopic } from '../../src/config'; import { ioRedisPool, setRedisObject } from '../../src/redis'; import { ReportReason } from '../../src/entity/common'; import { ContentPreferenceUser } from '../../src/entity/contentPreference/ContentPreferenceUser'; -import { ContentPreferenceStatus } from '../../src/entity/contentPreference/types'; +import { + ContentPreferenceStatus, + ContentPreferenceType, +} from '../../src/entity/contentPreference/types'; +import { ContentPreferenceSource } from '../../src/entity/contentPreference/ContentPreferenceSource'; let con: DataSource; @@ -65,6 +72,11 @@ beforeAll(async () => { beforeEach(async () => { jest.resetAllMocks(); await con.getRepository(User).save(usersFixture); + await saveFixtures( + con, + Feed, + usersFixture.map((u) => ({ id: u.id, userId: u.id })), + ); await con.getRepository(MachineSource).save(sourcesFixture); await con.getRepository(Post).save([postsFixture[0], postsFixture[1]]); await con.getRepository(Comment).save([ @@ -184,39 +196,69 @@ describe('squad featured updated notification', () => { beforeEach(async () => { await saveFixtures(con, User, badUsersFixture); + await saveFixtures( + con, + Feed, + badUsersFixture.map((u) => ({ id: u.id, userId: u.id })), + ); await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { - role: SourceMemberRoles.Admin, sourceId: 'a', + referenceId: 'a', userId: '1', - referralToken: 't1', + flags: { + role: SourceMemberRoles.Admin, + referralToken: 't1', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { - role: SourceMemberRoles.Admin, sourceId: 'a', + referenceId: 'a', userId: '2', - referralToken: 't2', + flags: { + role: SourceMemberRoles.Admin, + referralToken: 't2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '2', }, { - role: SourceMemberRoles.Moderator, sourceId: 'a', + referenceId: 'a', userId: '3', - referralToken: 't3', + flags: { + role: SourceMemberRoles.Moderator, + referralToken: 't3', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '3', }, { - role: SourceMemberRoles.Member, sourceId: 'a', + referenceId: 'a', userId: '4', - referralToken: 't4', + flags: { + role: SourceMemberRoles.Member, + referralToken: 't4', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '4', }, { - role: SourceMemberRoles.Blocked, sourceId: 'a', + referenceId: 'a', userId: 'low-score', - referralToken: 't5', + flags: { + role: SourceMemberRoles.Blocked, + referralToken: 't5', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: 'low-score', }, ]); }); @@ -255,9 +297,11 @@ describe('squad featured updated notification', () => { expect((actual![0].ctx as NotificationSourceContext).source.id).toEqual( source!.id, ); - const admins = await con.getRepository(SourceMember).findBy({ - role: SourceMemberRoles.Admin, - }); + const admins = await con + .getRepository(ContentPreferenceSource) + .createQueryBuilder() + .where(`flags->>'role' = :role`, { role: SourceMemberRoles.Admin }) + .getMany(); const adminsFound = admins.every((admin) => actual![0].ctx.userIds.includes(admin.userId), ); @@ -276,9 +320,11 @@ describe('squad featured updated notification', () => { expect((actual![0].ctx as NotificationSourceContext).source.id).toEqual( source!.id, ); - const mods = await con.getRepository(SourceMember).findBy({ - role: SourceMemberRoles.Moderator, - }); + const mods = await con + .getRepository(ContentPreferenceSource) + .createQueryBuilder() + .where(`flags->>'role' = :role`, { role: SourceMemberRoles.Moderator }) + .getMany(); const modsFound = mods.every((mod) => actual![0].ctx.userIds.includes(mod.userId), ); @@ -297,9 +343,11 @@ describe('squad featured updated notification', () => { expect((actual![0].ctx as NotificationSourceContext).source.id).toEqual( source!.id, ); - const members = await con.getRepository(SourceMember).findBy({ - role: SourceMemberRoles.Member, - }); + const members = await con + .getRepository(ContentPreferenceSource) + .createQueryBuilder() + .where(`flags->>'role' = :role`, { role: SourceMemberRoles.Member }) + .getMany(); const membersNotFound = members.every( (member) => !actual![0].ctx.userIds.includes(member.userId), ); @@ -318,9 +366,11 @@ describe('squad featured updated notification', () => { expect((actual![0].ctx as NotificationSourceContext).source.id).toEqual( source!.id, ); - const blocked = await con.getRepository(SourceMember).findBy({ - role: SourceMemberRoles.Blocked, - }); + const blocked = await con + .getRepository(ContentPreferenceSource) + .createQueryBuilder() + .where(`flags->>'role' = :role`, { role: SourceMemberRoles.Blocked }) + .getMany(); const blockedNotFound = blocked.every( (member) => !actual![0].ctx.userIds.includes(member.userId), ); @@ -329,10 +379,18 @@ describe('squad featured updated notification', () => { }); describe('source member role changed', () => { - const baseMember = { + const baseMember: ChangeObject = { userId: '1', sourceId: 'squad', - referralToken: 'rt1', + referenceId: 'squad', + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt1', + }, + feedId: '1', + status: ContentPreferenceStatus.Subscribed, + type: ContentPreferenceType.Source, + createdAt: Date.now(), }; it('should add blocked notification', async () => { @@ -341,7 +399,13 @@ describe('source member role changed', () => { ); const actual = await invokeNotificationWorker(worker.default, { previousRole: SourceMemberRoles.Member, - sourceMember: { ...baseMember, role: SourceMemberRoles.Blocked }, + sourceMember: { + ...baseMember, + flags: { + ...baseMember.flags, + role: SourceMemberRoles.Blocked, + }, + }, }); const source = await con.getRepository(Source).findOneBy({ id: 'squad' }); expect(actual.length).toEqual(1); @@ -354,7 +418,13 @@ describe('source member role changed', () => { ); const actual = await invokeNotificationWorker(worker.default, { previousRole: SourceMemberRoles.Member, - sourceMember: { ...baseMember, role: SourceMemberRoles.Moderator }, + sourceMember: { + ...baseMember, + flags: { + ...baseMember.flags, + role: SourceMemberRoles.Moderator, + }, + }, }); const source = await con.getRepository(Source).findOneBy({ id: 'squad' }); @@ -368,7 +438,13 @@ describe('source member role changed', () => { ); const actual = await invokeNotificationWorker(worker.default, { previousRole: SourceMemberRoles.Member, - sourceMember: { ...baseMember, role: SourceMemberRoles.Admin }, + sourceMember: { + ...baseMember, + flags: { + ...baseMember.flags, + role: SourceMemberRoles.Admin, + }, + }, }); const source = await con.getRepository(Source).findOneBy({ id: 'squad' }); @@ -385,7 +461,13 @@ describe('source member role changed', () => { ); const actual = await invokeNotificationWorker(worker.default, { previousRole: SourceMemberRoles.Moderator, - sourceMember: { ...baseMember, role: SourceMemberRoles.Member }, + sourceMember: { + ...baseMember, + flags: { + ...baseMember.flags, + role: SourceMemberRoles.Member, + }, + }, }); const source = await con.getRepository(Source).findOneBy({ id: 'squad' }); @@ -403,7 +485,13 @@ describe('source member role changed', () => { ); const actual = await invokeNotificationWorker(worker.default, { previousRole: SourceMemberRoles.Moderator, - sourceMember: { ...baseMember, role: SourceMemberRoles.Admin }, + sourceMember: { + ...baseMember, + flags: { + ...baseMember.flags, + role: SourceMemberRoles.Admin, + }, + }, }); const source = await con.getRepository(Source).findOneBy({ id: 'squad' }); expect(actual.length).toEqual(1); @@ -419,7 +507,13 @@ describe('source member role changed', () => { ); const actual = await invokeNotificationWorker(worker.default, { previousRole: SourceMemberRoles.Admin, - sourceMember: { ...baseMember, role: SourceMemberRoles.Member }, + sourceMember: { + ...baseMember, + flags: { + ...baseMember.flags, + role: SourceMemberRoles.Member, + }, + }, }); const source = await con.getRepository(Source).findOneBy({ id: 'squad' }); @@ -437,7 +531,13 @@ describe('source member role changed', () => { ); const actual = await invokeNotificationWorker(worker.default, { previousRole: SourceMemberRoles.Admin, - sourceMember: { ...baseMember, role: SourceMemberRoles.Moderator }, + sourceMember: { + ...baseMember, + flags: { + ...baseMember.flags, + role: SourceMemberRoles.Moderator, + }, + }, }); const source = await con.getRepository(Source).findOneBy({ id: 'squad' }); @@ -459,11 +559,16 @@ describe('post added notifications', () => { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); - await con.getRepository(SourceMember).save({ - userId: '2', + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', - role: SourceMemberRoles.Member, - referralToken: 'random', + referenceId: 'a', + userId: '2', + flags: { + role: SourceMemberRoles.Member, + referralToken: 'random', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '2', }); const actual = await invokeNotificationWorker(worker.default, { post: postsFixture[0], @@ -544,12 +649,16 @@ describe('post added notifications', () => { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); - await con.getRepository(SourceMember).insert({ - userId: '2', + await con.getRepository(ContentPreferenceSource).insert({ sourceId: 'a', - role: SourceMemberRoles.Blocked, - createdAt: new Date(), - referralToken: randomUUID(), + referenceId: 'a', + userId: '2', + flags: { + role: SourceMemberRoles.Blocked, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '2', }); const worker = await import('../../src/workers/notifications/postAdded'); await con.getRepository(Post).update({ id: 'p1' }, { authorId: '1' }); @@ -604,18 +713,28 @@ describe('post added notifications', () => { .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); await con.getRepository(Post).update({ id: 'p1' }, { authorId: '1' }); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { sourceId: 'a', + referenceId: 'a', userId: '2', - referralToken: 'rt1', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt1', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '2', }, { sourceId: 'a', + referenceId: 'a', userId: '3', - referralToken: 'rt2', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '3', }, ]); const actual = await invokeNotificationWorker(worker.default, { @@ -636,18 +755,28 @@ describe('post added notifications', () => { .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); await con.getRepository(Post).update({ id: 'p1' }, { authorId: '1' }); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { sourceId: 'a', + referenceId: 'a', userId: '2', - referralToken: 'rt1', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt1', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '2', }, { sourceId: 'a', + referenceId: 'a', userId: '3', - referralToken: 'rt2', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '3', }, ]); await con.getRepository(PostMention).save({ @@ -673,18 +802,28 @@ describe('post added notifications', () => { .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); await con.getRepository(Post).update({ id: 'p1' }, { authorId: '1' }); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { sourceId: 'a', + referenceId: 'a', userId: '2', - referralToken: 'rt1', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt1', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '2', }, { sourceId: 'a', + referenceId: 'a', userId: '3', - referralToken: 'rt2', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '3', }, ]); await con.getRepository(NotificationPreferenceSource).save({ @@ -1136,12 +1275,14 @@ describe('article new comment', () => { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); - await con - .getRepository(SourceMember) - .update( - { sourceId: 'a', userId: '1' }, - { role: SourceMemberRoles.Blocked }, - ); + await con.getRepository(ContentPreferenceSource).update( + { sourceId: 'a', userId: '1' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Blocked, + }), + }, + ); await con.getRepository(Post).update({ id: 'p1' }, { authorId: '1' }); const actual = await invokeNotificationWorker(worker.default, { userId: '1', @@ -1159,8 +1300,8 @@ describe('article new comment', () => { .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); await con - .getRepository(SourceMember) - .delete({ sourceId: 'a', userId: '1' }); + .getRepository(ContentPreferenceSource) + .delete({ referenceId: 'a', userId: '1' }); await con.getRepository(Post).update({ id: 'p1' }, { authorId: '1' }); const actual = await invokeNotificationWorker(worker.default, { userId: '1', @@ -1183,12 +1324,17 @@ describe('article new comment', () => { authorId: '1', }, ); - await con.getRepository(SourceMember).insert({ - userId: '1', + await con.getRepository(ContentPreferenceSource).insert({ sourceId: 'a', - role: SourceMemberRoles.Member, + referenceId: 'a', + userId: '1', createdAt: new Date(), - referralToken: randomUUID(), + flags: { + role: SourceMemberRoles.Member, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); const actual = await invokeNotificationWorker(worker.default, { userId: '1', @@ -1224,12 +1370,17 @@ describe('article new comment', () => { authorId: '1', }, ); - await con.getRepository(SourceMember).insert({ - userId: '1', + await con.getRepository(ContentPreferenceSource).insert({ sourceId: 'a', - role: SourceMemberRoles.Member, + referenceId: 'a', + userId: '1', createdAt: new Date(), - referralToken: randomUUID(), + flags: { + role: SourceMemberRoles.Member, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); await con.getRepository(NotificationPreferencePost).save({ userId: '1', @@ -1271,10 +1422,14 @@ describe('article upvote milestone', () => { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); - const repo = con.getRepository(SourceMember); + const repo = con.getRepository(ContentPreferenceSource); await repo.update( { userId: '1', sourceId: 'a' }, - { role: SourceMemberRoles.Blocked }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Blocked, + }), + }, ); await repo.delete({ userId: '3', sourceId: 'a' }); const actual = await invokeNotificationWorker(worker.default, { @@ -1743,7 +1898,7 @@ describe('comment upvote milestone', () => { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad }); - const repo = con.getRepository(SourceMember); + const repo = con.getRepository(ContentPreferenceSource); await con.getRepository(Comment).update({ id: 'c1' }, { upvotes: 3 }); await con.getRepository(UserComment).save([ { @@ -1754,7 +1909,11 @@ describe('comment upvote milestone', () => { { userId: '4', commentId: 'c1', vote: UserVote.Up }, ]); const params = { userId: '1', sourceId: 'a' }; - await repo.update(params, { role: SourceMemberRoles.Blocked }); + await repo.update(params, { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Blocked, + }), + }); const actual1 = await invokeNotificationWorker(worker.default, { userId: '1', commentId: 'c1', @@ -1831,12 +1990,17 @@ describe('squad member joined', () => { .update({ id: 'a' }, { type: SourceType.Squad }); const source = await con.getRepository(Source).findOneBy({ id: 'a' }); await createSquadWelcomePost(con, source as SquadSource, '1'); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { sourceId: 'a', + referenceId: 'a', userId: '2', - referralToken: 'rt1', - role: SourceMemberRoles.Admin, + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'rt1', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '2', }, ]); }; @@ -1850,6 +2014,7 @@ describe('squad member joined', () => { sourceMember: { sourceId: 'a', userId: '1', + flags: {}, }, }); expect(actual.length).toEqual(1); @@ -1866,11 +2031,16 @@ describe('squad member joined', () => { '../../src/workers/notifications/squadMemberJoined' ); await prepareSquad(); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', + referenceId: 'a', userId: '3', - role: SourceMemberRoles.Admin, - referralToken: 'token', + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'token', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '3', }); await con.getRepository(NotificationPreferenceSource).save({ userId: '3', @@ -1883,6 +2053,7 @@ describe('squad member joined', () => { sourceMember: { sourceId: 'a', userId: '1', + flags: {}, }, }); expect(actual.length).toEqual(1); @@ -1898,18 +2069,24 @@ describe('squad member joined', () => { const worker = await import( '../../src/workers/notifications/squadMemberJoined' ); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { sourceId: 'a', + referenceId: 'a', userId: '2', - referralToken: 'rt1', - role: SourceMemberRoles.Admin, + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'rt1', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '2', }, ]); const actual = await invokeNotificationWorker(worker.default, { sourceMember: { sourceId: 'a', userId: '2', + flags: {}, }, }); expect(actual).toBeFalsy(); @@ -1926,19 +2103,26 @@ describe('squad member joined', () => { const repo = con.getRepository(UserAction); const exists = await repo.findOneBy(params); expect(exists).toBeFalsy(); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { sourceId: 'a', + referenceId: 'a', userId: '2', - referralToken: 'rt1', - role, + flags: { + role, + referralToken: 'rt1', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '2', }, ]); await invokeNotificationWorker(worker.default, { sourceMember: { sourceId: 'a', userId: '2', - role, + flags: { + role, + }, }, }); @@ -1950,6 +2134,7 @@ describe('squad member joined', () => { sourceMember: { sourceId: 'a', userId: '2', + flags: {}, }, }); const createSquad = await repo.findOneBy(params); @@ -1972,6 +2157,7 @@ describe('squad member joined', () => { sourceMember: { sourceId: 'a', userId: '1', + flags: {}, }, }); expect(actual).toBeUndefined(); diff --git a/src/workers/notifications/postAdded.ts b/src/workers/notifications/postAdded.ts index 82d31248e..283018575 100644 --- a/src/workers/notifications/postAdded.ts +++ b/src/workers/notifications/postAdded.ts @@ -83,7 +83,7 @@ const worker: NotificationWorker = { source.id, (qb) => qb - .where(`${qb.alias}."userId" NOT IN (:...users)`, { + .andWhere(`${qb.alias}."userId" NOT IN (:...users)`, { users: [ post.authorId, ...mentions.flatMap(({ mentionedUserId }) => mentionedUserId), diff --git a/src/workers/notifications/squadMemberJoined.ts b/src/workers/notifications/squadMemberJoined.ts index 7f8832d94..4e7a5f9df 100644 --- a/src/workers/notifications/squadMemberJoined.ts +++ b/src/workers/notifications/squadMemberJoined.ts @@ -29,7 +29,7 @@ const worker: NotificationWorker = { member.sourceId, (qb) => qb - .where(`${qb.alias}."userId" NOT IN (:...users)`, { + .andWhere(`${qb.alias}."userId" NOT IN (:...users)`, { users: [member.userId], }) .andWhere(`${qb.alias}."referenceId" = :sourceId`, { diff --git a/src/workers/notifications/utils.ts b/src/workers/notifications/utils.ts index 64b2f8680..59374155e 100644 --- a/src/workers/notifications/utils.ts +++ b/src/workers/notifications/utils.ts @@ -24,7 +24,6 @@ import { import { DataSource, EntityManager, In, SelectQueryBuilder } from 'typeorm'; import { SourceMemberRoles } from '../../roles'; import { insertOrIgnoreAction } from '../../schema/actions'; -import { ObjectLiteral } from 'typeorm/common/ObjectLiteral'; import { SourcePostModeration } from '../../entity/SourcePostModeration'; import { ChangeObject } from '../../types'; import { ContentPreferenceSource } from '../../entity/contentPreference/ContentPreferenceSource'; @@ -39,18 +38,18 @@ export const uniquePostOwners = ( type GetSubscribedMembersWhereBuilder = ( qb: SelectQueryBuilder, -) => string; +) => SelectQueryBuilder; export const getSubscribedMembers = ( con: DataSource, type: NotificationType, referenceId: string, - where: ObjectLiteral | GetSubscribedMembersWhereBuilder, + where: GetSubscribedMembersWhereBuilder, ) => { const builder = con .getRepository(ContentPreferenceSource) .createQueryBuilder('cps'); - const memberQuery = builder.select('"userId"').where(where); + const memberQuery = where(builder.select('"userId"')); const muteQuery = builder .subQuery() .select('np."userId"') @@ -159,7 +158,7 @@ export async function articleNewCommentHandler( if (source.type === SourceType.Squad) { const members = await getSubscribedMembers(con, type, post.id, (qb) => qb - .where(`${qb.alias}."userId" IN (:...users)`, { + .andWhere(`${qb.alias}."userId" IN (:...users)`, { users, }) .andWhere(`${qb.alias}."referenceId" = :sourceId`, { From c10a83de7f25728c69ffce4b7eb353a884a0389c Mon Sep 17 00:00:00 2001 From: capJavert Date: Mon, 18 Nov 2024 13:16:32 +0100 Subject: [PATCH 5/9] feat: post tests --- __tests__/posts.ts | 477 +++++++++++++++++++++++++++++++-------------- 1 file changed, 330 insertions(+), 147 deletions(-) diff --git a/__tests__/posts.ts b/__tests__/posts.ts index 4864ebffd..e92ddb287 100644 --- a/__tests__/posts.ts +++ b/__tests__/posts.ts @@ -15,6 +15,7 @@ import { Bookmark, BookmarkList, Comment, + Feed, FreeformPost, Post, PostMention, @@ -26,7 +27,6 @@ import { PostType, SharePost, Source, - SourceMember, SourceType, SquadSource, UNKNOWN_SOURCE, @@ -81,6 +81,8 @@ import { } from '../src/entity/SourcePostModeration'; import { generateUUID } from '../src/ids'; import { GQLResponse } from 'mercurius-integration-testing'; +import { ContentPreferenceSource } from '../src/entity/contentPreference/ContentPreferenceSource'; +import { ContentPreferenceStatus } from '../src/entity/contentPreference/types'; jest.mock('../src/common/pubsub', () => ({ ...(jest.requireActual('../src/common/pubsub') as Record), @@ -115,6 +117,11 @@ beforeEach(async () => { await saveFixtures(con, YouTubePost, videoPostsFixture); await saveFixtures(con, PostTag, postTagsFixture); await saveFixtures(con, User, badUsersFixture); + await saveFixtures( + con, + Feed, + badUsersFixture.map((u) => ({ id: u.id, userId: u.id })), + ); await con .getRepository(User) .save({ id: '1', name: 'Ido', image: 'https://daily.dev/ido.jpg' }); @@ -142,6 +149,13 @@ beforeEach(async () => { name: 'Joanna Deer', }, ]); + await saveFixtures(con, Feed, [ + { id: '1', userId: '1' }, + { id: '2', userId: '2' }, + { id: '3', userId: '3' }, + { id: '4', userId: '4' }, + { id: '5', userId: '5' }, + ]); await con.getRepository(SquadSource).save({ id: 'm', name: 'Moderated Squad', @@ -154,24 +168,39 @@ beforeEach(async () => { memberPostingRank: sourceRoleRank[SourceMemberRoles.Member], memberInviteRank: sourceRoleRank[SourceMemberRoles.Member], }); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { - userId: '3', sourceId: 'm', - role: SourceMemberRoles.Moderator, - referralToken: randomUUID(), + referenceId: 'm', + userId: '3', + flags: { + role: SourceMemberRoles.Moderator, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '3', }, { - userId: '4', sourceId: 'm', - role: SourceMemberRoles.Member, - referralToken: randomUUID(), + referenceId: 'm', + userId: '4', + flags: { + role: SourceMemberRoles.Member, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '4', }, { - userId: '5', sourceId: 'm', - role: SourceMemberRoles.Member, - referralToken: randomUUID(), + referenceId: 'm', + userId: '5', + flags: { + role: SourceMemberRoles.Member, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '5', }, ]); await deleteKeysByPattern(`${rateLimiterName}:*`); @@ -188,26 +217,41 @@ const saveSquadFixtures = async () => { { id: 'p1' }, { type: PostType.Welcome, title: 'Welcome post', authorId: '1' }, ); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { - userId: '1', sourceId: 'a', - role: SourceMemberRoles.Member, - referralToken: randomUUID(), + referenceId: 'a', + userId: '1', + flags: { + role: SourceMemberRoles.Member, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { - userId: '2', sourceId: 'a', - role: SourceMemberRoles.Member, - referralToken: randomUUID(), + referenceId: 'a', + userId: '2', + flags: { + role: SourceMemberRoles.Member, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '2', }, ]); - await con.getRepository(SourceMember).save( + await con.getRepository(ContentPreferenceSource).save( badUsersFixture.map((user) => ({ - userId: user.id, sourceId: 'a', - role: SourceMemberRoles.Member, - referralToken: randomUUID(), + referenceId: 'a', + userId: user.id, + flags: { + role: SourceMemberRoles.Member, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: user.id, })), ); }; @@ -1006,11 +1050,16 @@ describe('query post', () => { it('should throw error when annonymous user tries to access post from source with members', async () => { await con.getRepository(Source).update({ id: 'a' }, { private: true }); await con.getRepository(Post).update({ id: 'p1' }, { private: true }); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', + referenceId: 'a', userId: '1', - referralToken: 'rt2', - role: SourceMemberRoles.Admin, + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); return testQueryErrorCode( client, @@ -1025,11 +1074,16 @@ describe('query post', () => { loggedUser = '2'; await con.getRepository(Source).update({ id: 'a' }, { private: true }); await con.getRepository(Post).update({ id: 'p1' }, { private: true }); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', + referenceId: 'a', userId: '1', - referralToken: 'rt2', - role: SourceMemberRoles.Admin, + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); return testQueryErrorCode( client, @@ -1053,11 +1107,16 @@ describe('query post', () => { await con .getRepository(Post) .update({ id: 'p1' }, { private: false, sourceId: 'a' }); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', + referenceId: 'a', userId: '1', - referralToken: 'rt2', - role: SourceMemberRoles.Blocked, + flags: { + role: SourceMemberRoles.Blocked, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); return testQueryErrorCode( @@ -1442,23 +1501,34 @@ describe('mutation deletePost', () => { const createSharedPost = async ( id = 'sp1', - member: Partial = {}, + member: Partial = {}, authorId = '2', ) => { const post = await con.getRepository(Post).findOneBy({ id: 'p1' }); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { - userId: '1', sourceId: 'a', - role: SourceMemberRoles.Member, - referralToken: randomUUID(), + referenceId: 'a', + userId: '1', + flags: { + role: SourceMemberRoles.Member, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { - userId: '2', sourceId: 'a', - role: SourceMemberRoles.Member, - referralToken: randomUUID(), + referenceId: 'a', + userId: '2', + status: ContentPreferenceStatus.Subscribed, + feedId: '2', ...member, + flags: { + role: SourceMemberRoles.Member, + referralToken: randomUUID(), + ...member.flags, + }, }, ]); await con.getRepository(SharePost).save({ @@ -1497,7 +1567,11 @@ describe('mutation deletePost', () => { it('should restrict member deleting a post from a moderator', async () => { loggedUser = '1'; const id = 'sp1'; - await createSharedPost(id, { role: SourceMemberRoles.Moderator }); + await createSharedPost(id, { + flags: { + role: SourceMemberRoles.Moderator, + }, + }); return testMutationErrorCode( client, @@ -1509,7 +1583,11 @@ describe('mutation deletePost', () => { it('should restrict member deleting a post from the admin', async () => { loggedUser = '1'; const id = 'sp1'; - await createSharedPost(id, { role: SourceMemberRoles.Admin }); + await createSharedPost(id, { + flags: { + role: SourceMemberRoles.Admin, + }, + }); return testMutationErrorCode( client, @@ -1558,21 +1636,28 @@ describe('mutation deletePost', () => { it('should delete the welcome post by a moderator or an admin', async () => { loggedUser = '2'; - await con.getRepository(SourceMember).save({ - userId: '1', + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', - role: SourceMemberRoles.Moderator, - referralToken: randomUUID(), + referenceId: 'a', + userId: '1', + flags: { + role: SourceMemberRoles.Moderator, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); const source = await con.getRepository(Source).findOneBy({ id: 'a' }); const post = await createSquadWelcomePost(con, source, '2'); await verifyPostDeleted(post.id, loggedUser); - await con - .getRepository(SourceMember) - .update( - { userId: '1', sourceId: 'a' }, - { role: SourceMemberRoles.Admin }, - ); + await con.getRepository(ContentPreferenceSource).update( + { userId: '1', referenceId: 'a' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Admin, + }), + }, + ); const welcome = await createSquadWelcomePost(con, source, '2'); await con .getRepository(Post) @@ -1583,17 +1668,34 @@ describe('mutation deletePost', () => { it('should delete the shared post from a member as a moderator', async () => { loggedUser = '2'; const id = 'sp1'; - await createSharedPost(id, { role: SourceMemberRoles.Moderator }, '1'); + await createSharedPost( + id, + { + flags: { + role: SourceMemberRoles.Moderator, + }, + }, + '1', + ); await verifyPostDeleted(id, loggedUser); }); it('should allow moderator deleting a post from other moderators', async () => { loggedUser = '1'; const id = 'sp1'; - await createSharedPost(id, { role: SourceMemberRoles.Moderator }); - await con - .getRepository(SourceMember) - .update({ userId: '1' }, { role: SourceMemberRoles.Moderator }); + await createSharedPost(id, { + flags: { + role: SourceMemberRoles.Moderator, + }, + }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '1' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), + }, + ); await verifyPostDeleted(id, loggedUser); }); @@ -1601,10 +1703,19 @@ describe('mutation deletePost', () => { it('should allow moderator deleting a post from the admin', async () => { loggedUser = '1'; const id = 'sp1'; - await createSharedPost(id, { role: SourceMemberRoles.Admin }); - await con - .getRepository(SourceMember) - .update({ userId: '1' }, { role: SourceMemberRoles.Moderator }); + await createSharedPost(id, { + flags: { + role: SourceMemberRoles.Admin, + }, + }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '1' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), + }, + ); await verifyPostDeleted(id, loggedUser); }); @@ -1612,7 +1723,15 @@ describe('mutation deletePost', () => { it('should delete the shared post as an admin of the squad', async () => { loggedUser = '2'; const id = 'sp1'; - await createSharedPost(id, { role: SourceMemberRoles.Admin }, '1'); + await createSharedPost( + id, + { + flags: { + role: SourceMemberRoles.Admin, + }, + }, + '1', + ); await verifyPostDeleted(id, loggedUser); }); }); @@ -1884,11 +2003,16 @@ describe('mutation sharePost', () => { private: false, memberPostingRank: 0, }); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: 's1', + referenceId: 's1', userId: '1', - referralToken: 'rt', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); }); @@ -2061,10 +2185,12 @@ describe('mutation sharePost', () => { await con.getRepository(SquadSource).update('s1', { memberPostingRank: sourceRoleRank[SourceMemberRoles.Moderator], }); - await con.getRepository(SourceMember).update( - { sourceId: 's1', userId: '1' }, + await con.getRepository(ContentPreferenceSource).update( + { referenceId: 's1', userId: '1' }, { - role: SourceMemberRoles.Moderator, + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), }, ); @@ -2082,10 +2208,12 @@ describe('mutation sharePost', () => { await con.getRepository(SquadSource).update('s1', { memberPostingRank: sourceRoleRank[SourceMemberRoles.Moderator], }); - await con.getRepository(SourceMember).update( - { sourceId: 's1', userId: '1' }, + await con.getRepository(ContentPreferenceSource).update( + { referenceId: 's1', userId: '1' }, { - role: SourceMemberRoles.Admin, + flags: updateFlagsStatement({ + role: SourceMemberRoles.Admin, + }), }, ); @@ -2144,11 +2272,16 @@ describe('mutation sharePost', () => { name: 'Watercooler', private: false, }); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: WATERCOOLER_ID, + referenceId: WATERCOOLER_ID, userId: '1', - referralToken: 'watercoolerRt', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'watercoolerRt', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); }); @@ -2309,11 +2442,16 @@ describe('mutation viewPost', () => { name: 'Squad', private: true, }); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: 's1', + referenceId: 's1', userId: '1', - referralToken: 'rt', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); await con.getRepository(Post).update({ id: 'p1' }, { sourceId: 's1' }); }); @@ -2395,11 +2533,16 @@ describe('mutation submitExternalLink', () => { private: false, memberPostingRank: 0, }); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: 's1', + referenceId: 's1', userId: '1', - referralToken: 'rt', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); }); @@ -2625,10 +2768,12 @@ describe('mutation submitExternalLink', () => { await con.getRepository(SquadSource).update('s1', { memberPostingRank: sourceRoleRank[SourceMemberRoles.Moderator], }); - await con.getRepository(SourceMember).update( - { sourceId: 's1', userId: '1' }, + await con.getRepository(ContentPreferenceSource).update( + { referenceId: 's1', userId: '1' }, { - role: SourceMemberRoles.Moderator, + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), }, ); @@ -2658,10 +2803,12 @@ describe('mutation submitExternalLink', () => { await con.getRepository(SquadSource).update('s1', { memberPostingRank: sourceRoleRank[SourceMemberRoles.Moderator], }); - await con.getRepository(SourceMember).update( - { sourceId: 's1', userId: '1' }, + await con.getRepository(ContentPreferenceSource).update( + { referenceId: 's1', userId: '1' }, { - role: SourceMemberRoles.Admin, + flags: updateFlagsStatement({ + role: SourceMemberRoles.Admin, + }), }, ); @@ -2766,11 +2913,16 @@ describe('mutation submitExternalLink', () => { name: 'Watercooler', private: false, }); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: WATERCOOLER_ID, + referenceId: WATERCOOLER_ID, userId: '1', - referralToken: 'watercoolerRt', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'watercoolerRt', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); }); @@ -2833,11 +2985,16 @@ describe('mutation submitExternalLink', () => { it('should set the correct vordr flags on new post by a bad user', async () => { loggedUser = 'vordr'; - await con.getRepository(SourceMember).save({ - userId: loggedUser, + await con.getRepository(ContentPreferenceSource).save({ sourceId: 's1', - role: SourceMemberRoles.Member, - referralToken: randomUUID(), + referenceId: 's1', + userId: loggedUser, + flags: { + role: SourceMemberRoles.Member, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: loggedUser, }); const res = await client.mutate(MUTATION, { @@ -2879,11 +3036,16 @@ describe('mutation submitExternalLink', () => { it('should set the correct vordr flags on existing post by bad user', async () => { loggedUser = 'vordr'; - await con.getRepository(SourceMember).save({ - userId: loggedUser, + await con.getRepository(ContentPreferenceSource).save({ sourceId: 's1', - role: SourceMemberRoles.Member, - referralToken: randomUUID(), + referenceId: 's1', + userId: loggedUser, + flags: { + role: SourceMemberRoles.Member, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: loggedUser, }); const res = await client.mutate(MUTATION, { @@ -3385,11 +3547,16 @@ describe('mutation createFreeformPost', () => { name: 'Watercooler', private: false, }); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: WATERCOOLER_ID, + referenceId: WATERCOOLER_ID, userId: '1', - referralToken: 'watercoolerRt', - role: SourceMemberRoles.Member, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'watercoolerRt', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); }); @@ -4028,12 +4195,14 @@ describe('mutation editPost', () => { await con .getRepository(Post) .update({ id: 'p1' }, { type: PostType.Freeform, authorId: '2' }); - await con - .getRepository(SourceMember) - .update( - { userId: '1', sourceId: 'a' }, - { role: SourceMemberRoles.Moderator }, - ); + await con.getRepository(ContentPreferenceSource).update( + { userId: '1', referenceId: 'a' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), + }, + ); const title = 'Updated title'; await testMutationErrorCode( client, @@ -4044,12 +4213,14 @@ describe('mutation editPost', () => { 'FORBIDDEN', ); - await con - .getRepository(SourceMember) - .update( - { userId: '1', sourceId: 'a' }, - { role: SourceMemberRoles.Admin }, - ); + await con.getRepository(ContentPreferenceSource).update( + { userId: '1', referenceId: 'a' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Admin, + }), + }, + ); return await testMutationErrorCode( client, @@ -4064,12 +4235,14 @@ describe('mutation editPost', () => { it('should allow moderator to do update of welcome posts', async () => { loggedUser = '2'; - await con - .getRepository(SourceMember) - .update( - { userId: '2', sourceId: 'a' }, - { role: SourceMemberRoles.Moderator }, - ); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2', referenceId: 'a' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), + }, + ); const content = 'Updated content'; const res = await client.mutate(MUTATION, { @@ -4082,12 +4255,14 @@ describe('mutation editPost', () => { it('should allow admin to do update of welcome post', async () => { loggedUser = '2'; - await con - .getRepository(SourceMember) - .update( - { userId: '2', sourceId: 'a' }, - { role: SourceMemberRoles.Admin }, - ); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2', referenceId: 'a' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Admin, + }), + }, + ); const content = 'Updated content'; const res = await client.mutate(MUTATION, { @@ -4139,12 +4314,14 @@ describe('mutation editPost', () => { content: '#Test', contentHtml: '

Test

', }); - await con - .getRepository(SourceMember) - .update( - { userId: '2', sourceId: 'a' }, - { role: SourceMemberRoles.Admin }, - ); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2', referenceId: 'a' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Admin, + }), + }, + ); const res = await client.mutate(MUTATION, { variables: { @@ -4286,9 +4463,14 @@ describe('mutation updatePinPost', () => { it('should update pinnedAt property based on the parameter if user is admin or moderator', async () => { loggedUser = '1'; - await con - .getRepository(SourceMember) - .update({ userId: '1' }, { role: SourceMemberRoles.Admin }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '1' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Admin, + }), + }, + ); const getPost = () => con.getRepository(Post).findOneBy({ id: 'p1' }); @@ -4309,9 +4491,14 @@ describe('mutation updatePinPost', () => { const unpinnedAgain = await getPost(); expect(unpinnedAgain.pinnedAt).toBeNull(); - await con - .getRepository(SourceMember) - .update({ userId: '1' }, { role: SourceMemberRoles.Moderator }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '1' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), + }, + ); await client.mutate(MUTATION, { variables: { id: 'p1', pinned: true }, @@ -4372,9 +4559,14 @@ describe('mutation swapPinnedPosts', () => { describe('when authenticated w/ permissions', () => { beforeEach(async () => { - await con - .getRepository(SourceMember) - .update({ userId: '1' }, { role: SourceMemberRoles.Admin }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '1' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Admin, + }), + }, + ); }); it('should throw a validation error if posts are not pinned', async () => { @@ -5497,7 +5689,6 @@ describe('query postCodeSnippets', () => { variables: { id: 'p1' }, }); - console.log(JSON.stringify(res.data, null, 2)); expect(res.errors).toBeFalsy(); expect(res.data).toMatchObject({ postCodeSnippets: { @@ -5689,14 +5880,6 @@ describe('Source post moderation edit/delete', () => { beforeEach(async () => { await saveSquadFixtures(); - await con.getRepository(SourceMember).save([ - { - sourceId: 'm', - userId: '4', - role: SourceMemberRoles.Member, - referralToken: 'r4', - }, - ]); await con.getRepository(SourcePostModeration).save([ { id: pendingId, From 4878e615b5b405a82e17b3bdc7224df7f990dedd Mon Sep 17 00:00:00 2001 From: capJavert Date: Mon, 18 Nov 2024 15:05:17 +0100 Subject: [PATCH 6/9] feat: source tests --- __tests__/sources.ts | 740 ++++++++++++++++++++++++++++++++---------- src/graphorm/index.ts | 16 +- src/schema/sources.ts | 26 ++ 3 files changed, 607 insertions(+), 175 deletions(-) diff --git a/__tests__/sources.ts b/__tests__/sources.ts index d8957c3d2..d2ceb8526 100644 --- a/__tests__/sources.ts +++ b/__tests__/sources.ts @@ -43,7 +43,10 @@ import { testQueryErrorCode, } from './helpers'; import { ContentPreferenceSource } from '../src/entity/contentPreference/ContentPreferenceSource'; -import { ContentPreferenceStatus } from '../src/entity/contentPreference/types'; +import { + ContentPreferenceStatus, + ContentPreferenceType, +} from '../src/entity/contentPreference/types'; import { generateUUID } from '../src/ids'; import { SourcePostModeration, @@ -187,6 +190,100 @@ beforeEach(async () => { await con .getRepository(SourceMember) .update({ userId: '2', sourceId: 'b' }, { role: SourceMemberRoles.Admin }); + + await con.getRepository(ContentPreferenceSource).save([ + { + sourceId: 'a', + referenceId: 'a', + userId: '1', + createdAt: new Date(now.getTime() + 0), + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', + }, + { + sourceId: 'a', + referenceId: 'a', + userId: '2', + createdAt: new Date(now.getTime() + 1000), + flags: { + role: SourceMemberRoles.Member, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '2', + }, + { + sourceId: 'b', + referenceId: 'b', + userId: '2', + createdAt: new Date(now.getTime() + 2000), + flags: { + role: SourceMemberRoles.Member, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '3', + }, + { + sourceId: 'b', + referenceId: 'b', + userId: '3', + createdAt: new Date(now.getTime() + 3000), + flags: { + role: SourceMemberRoles.Member, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '3', + }, + { + sourceId: 'squad', + referenceId: 'squad', + userId: '1', + createdAt: new Date(now.getTime() + 4000), + flags: { + role: SourceMemberRoles.Member, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', + }, + { + sourceId: 'm', + referenceId: 'm', + userId: '1', + createdAt: new Date(now.getTime() + 5000), + flags: { + role: SourceMemberRoles.Admin, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', + }, + ]); + + await con.getRepository(ContentPreferenceSource).update( + { + userId: '1', + }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Admin, + }), + }, + ); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2', referenceId: 'b' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Admin, + }), + }, + ); }); afterAll(() => disposeGraphQLTesting(state)); @@ -947,27 +1044,42 @@ query Source($id: ID!) { it('should return current member as admin', async () => { loggedUser = '1'; - await con - .getRepository(SourceMember) - .update({ userId: '1' }, { role: SourceMemberRoles.Admin }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '1' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Admin, + }), + }, + ); const res = await client.query(QUERY, { variables: { id: 'a' } }); expect(res.data).toMatchSnapshot(); }); it('should return current member as member', async () => { loggedUser = '1'; - await con - .getRepository(SourceMember) - .update({ userId: '1' }, { role: SourceMemberRoles.Member }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '1' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Member, + }), + }, + ); const res = await client.query(QUERY, { variables: { id: 'a' } }); expect(res.data).toMatchSnapshot(); }); it('should return current member as blocked', async () => { loggedUser = '1'; - await con - .getRepository(SourceMember) - .update({ userId: '1' }, { role: SourceMemberRoles.Blocked }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '1' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Blocked, + }), + }, + ); const res = await client.query(QUERY, { variables: { id: 'a' } }); expect(res.data.source).toBeNull(); }); @@ -980,12 +1092,17 @@ query Source($id: ID!) { name: 'Restricted Squad', memberPostingRank: sourceRoleRank[SourceMemberRoles.Moderator], }); - await con.getRepository(SourceMember).save({ - userId: '1', + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'restrictedsquad1', - role: SourceMemberRoles.Member, - referralToken: 'restrictedsquadtoken', + referenceId: 'restrictedsquad1', + userId: '1', createdAt: new Date(2022, 11, 19), + flags: { + role: SourceMemberRoles.Member, + referralToken: 'restrictedsquadtoken', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); const res = await client.query(QUERY, { variables: { id: 'restrictedsquad1' }, @@ -1005,12 +1122,17 @@ query Source($id: ID!) { name: 'Restricted Squad', memberInviteRank: sourceRoleRank[SourceMemberRoles.Moderator], }); - await con.getRepository(SourceMember).save({ - userId: '1', + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'restrictedsquad1', - role: SourceMemberRoles.Member, - referralToken: 'restrictedsquadtoken', + referenceId: 'restrictedsquad1', + userId: '1', createdAt: new Date(2022, 11, 19), + flags: { + role: SourceMemberRoles.Member, + referralToken: 'restrictedsquadtoken', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }); const res = await client.query(QUERY, { variables: { id: 'restrictedsquad1' }, @@ -1055,9 +1177,14 @@ query Source($id: ID!) { it('should not return private source when user is blocked', async () => { loggedUser = '1'; - await con - .getRepository(SourceMember) - .update({ userId: '1' }, { role: SourceMemberRoles.Blocked }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '1' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Blocked, + }), + }, + ); await con.getRepository(Source).update({ id: 'a' }, { private: true }); return testQueryErrorCode( client, @@ -1118,11 +1245,17 @@ query Source($id: ID!) { { id: 'a' }, { handle: 'handle', private: false, type: SourceType.Squad }, ); - await con.getRepository(SourceMember).save({ - userId: '1', + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', - role: SourceMemberRoles.Member, - referralToken: 'referraltoken1', + referenceId: 'a', + userId: '1', + flags: { + role: SourceMemberRoles.Member, + referralToken: 'referraltoken1', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', + type: ContentPreferenceType.Source, }); const res = await client.query(QUERY, { variables: { id: 'handle' } }); expect(res.errors).toBeFalsy(); @@ -1152,11 +1285,17 @@ query Source($id: ID!) { { id: 'a' }, { handle: 'handle', private: true, type: SourceType.Squad }, ); - await con.getRepository(SourceMember).save({ - userId: '1', + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', - role: SourceMemberRoles.Member, - referralToken: 'referraltoken1', + referenceId: 'a', + userId: '1', + flags: { + role: SourceMemberRoles.Member, + referralToken: 'referraltoken1', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', + type: ContentPreferenceType.Source, }); const res = await client.query(QUERY, { variables: { id: 'handle' } }); expect(res.errors).toBeFalsy(); @@ -1170,11 +1309,17 @@ query Source($id: ID!) { await con .getRepository(Source) .update({ id: 'a' }, { type: SourceType.Squad, private: false }); - await con.getRepository(SourceMember).save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'a', + referenceId: 'a', userId: '1', - referralToken: 'rt2', - role: SourceMemberRoles.Blocked, + flags: { + role: SourceMemberRoles.Blocked, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', + type: ContentPreferenceType.Source, }); return testQueryErrorCode( @@ -1197,11 +1342,17 @@ describe('query source moderation fields', () => { moderationRequired: true, }, ); - await con.getRepository(SourceMember).save({ - userId: '2', + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'm', - role: SourceMemberRoles.Member, - referralToken: generateUUID(), + referenceId: 'm', + userId: '2', + flags: { + role: SourceMemberRoles.Member, + referralToken: generateUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '2', + type: ContentPreferenceType.Source, }); await con.getRepository(SourcePostModeration).save({ sourceId: 'm', @@ -1266,11 +1417,17 @@ query Source($id: ID!) { it('should return only my moderationPostCount', async () => { loggedUser = '3'; - await con.getRepository(SourceMember).save({ - userId: '3', + await con.getRepository(ContentPreferenceSource).save({ sourceId: 'm', - role: SourceMemberRoles.Member, - referralToken: generateUUID(), + referenceId: 'm', + userId: '3', + flags: { + role: SourceMemberRoles.Member, + referralToken: generateUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '3', + type: ContentPreferenceType.Source, }); const res = await client.query(QUERY, { variables: { id: 'm' } }); expect(res.errors).toBeFalsy(); @@ -1375,9 +1532,14 @@ query Source($id: ID!) { }); it('should exclude blocked members from result', async () => { - await con - .getRepository(SourceMember) - .update({ userId: '2' }, { role: SourceMemberRoles.Blocked }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Blocked, + }), + }, + ); const res = await client.query(QUERY, { variables: { id: 'a' } }); expect(res.errors).toBeFalsy(); expect(res.data).toMatchSnapshot(); @@ -1444,6 +1606,14 @@ query Source($id: ID!) { await con .getRepository(SourceMember) .update({ userId: '2' }, { role: SourceMemberRoles.Blocked }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Blocked, + }), + }, + ); loggedUser = '1'; const res = await client.query(QUERY, { variables: { id: 'a' } }); expect(res.errors).toBeFalsy(); @@ -1489,9 +1659,14 @@ describe('query sourceMembers', () => { }); it('should return source members of public source without blocked members', async () => { - await con - .getRepository(SourceMember) - .update({ userId: '2' }, { role: SourceMemberRoles.Blocked }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Blocked, + }), + }, + ); const res = await client.query(QUERY, { variables: { id: 'a' } }); expect(res.errors).toBeFalsy(); expect(res.data).toMatchSnapshot(); @@ -1517,10 +1692,16 @@ describe('query sourceMembers', () => { }); it('should return source members and order by their role', async () => { - const repo = con.getRepository(SourceMember); + const repo = con.getRepository(ContentPreferenceSource); await repo.update( { userId: '3' }, - { role: SourceMemberRoles.Member, sourceId: 'a' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Member, + }), + sourceId: 'a', + referenceId: 'a', + }, ); const noModRes = await client.query(QUERY, { variables: { id: 'a' } }); expect(noModRes.errors).toBeFalsy(); @@ -1532,7 +1713,13 @@ describe('query sourceMembers', () => { await repo.update( { userId: '3' }, - { role: SourceMemberRoles.Moderator, sourceId: 'a' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), + sourceId: 'a', + referenceId: 'a', + }, ); const res = await client.query(QUERY, { variables: { id: 'a' } }); @@ -1563,12 +1750,14 @@ describe('query sourceMembers', () => { it('should not return blocked source members when user is not a moderator/admin', async () => { loggedUser = '2'; - await con - .getRepository(SourceMember) - .update( - { userId: '1', sourceId: 'a' }, - { role: SourceMemberRoles.Blocked }, - ); + await con.getRepository(ContentPreferenceSource).update( + { userId: '1', referenceId: 'a' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Blocked, + }), + }, + ); return testQueryErrorCode( client, { query: QUERY, variables: { role: SourceMemberRoles.Blocked, id: 'a' } }, @@ -1578,12 +1767,14 @@ describe('query sourceMembers', () => { it('should return blocked users only when user is the admin', async () => { loggedUser = '1'; - await con - .getRepository(SourceMember) - .update( - { userId: '2', sourceId: 'a' }, - { role: SourceMemberRoles.Blocked }, - ); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2', referenceId: 'a' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Blocked, + }), + }, + ); const res = await client.query(QUERY, { variables: { role: SourceMemberRoles.Blocked, id: 'a' }, }); @@ -1593,18 +1784,22 @@ describe('query sourceMembers', () => { it('should return blocked users only when user is a moderator', async () => { loggedUser = '1'; - await con - .getRepository(SourceMember) - .update( - { userId: '2', sourceId: 'a' }, - { role: SourceMemberRoles.Blocked }, - ); - await con - .getRepository(SourceMember) - .update( - { userId: '1', sourceId: 'a' }, - { role: SourceMemberRoles.Moderator }, - ); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2', referenceId: 'a' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Blocked, + }), + }, + ); + await con.getRepository(ContentPreferenceSource).update( + { userId: '1', referenceId: 'a' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), + }, + ); const res = await client.query(QUERY, { variables: { role: SourceMemberRoles.Blocked, id: 'a' }, }); @@ -1650,12 +1845,14 @@ describe('query sourceMembers', () => { describe('query mySourceMemberships', () => { afterEach(async () => { - await con - .getRepository(SourceMember) - .update( - { userId: '2', sourceId: 'a' }, - { role: SourceMemberRoles.Member }, - ); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2', referenceId: 'a' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Member, + }), + }, + ); }); const createQuery = (type?: SourceType) => ` @@ -1693,12 +1890,14 @@ describe('query mySourceMemberships', () => { }); it('should not return source memberships if user is blocked', async () => { - await con - .getRepository(SourceMember) - .update( - { userId: '2', sourceId: 'a' }, - { role: SourceMemberRoles.Blocked }, - ); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2', referenceId: 'a' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Blocked, + }), + }, + ); loggedUser = '2'; const res = await client.query(QUERY); expect(res.errors).toBeFalsy(); @@ -1981,11 +2180,12 @@ describe('mutation createSquad', () => { sourceRoleRank[SourceMemberRoles.Member], ); expect(newSource?.moderationRequired).toEqual(false); - const member = await con.getRepository(SourceMember).findOneBy({ + const member = await con.getRepository(ContentPreferenceSource).findOneBy({ + referenceId: newId, sourceId: newId, userId: '1', }); - expect(member.role).toEqual(SourceMemberRoles.Admin); + expect(member?.flags.role).toEqual(SourceMemberRoles.Admin); const sharePost = await con .getRepository(SharePost) .findOneBy({ sourceId: newId }); @@ -2135,11 +2335,12 @@ describe('mutation createSquad', () => { expect(newSource?.memberPostingRank).toEqual( sourceRoleRank[SourceMemberRoles.Moderator], ); - const member = await con.getRepository(SourceMember).findOneBy({ + const member = await con.getRepository(ContentPreferenceSource).findOneBy({ + referenceId: newId, sourceId: newId, userId: '1', }); - expect(member.role).toEqual(SourceMemberRoles.Admin); + expect(member?.flags.role).toEqual(SourceMemberRoles.Admin); const post = await con .getRepository(SharePost) .findOneBy({ sourceId: newId }); @@ -2235,11 +2436,12 @@ describe('mutation createSquad', () => { expect(newSource?.memberInviteRank).toEqual( sourceRoleRank[SourceMemberRoles.Moderator], ); - const member = await con.getRepository(SourceMember).findOneBy({ + const member = await con.getRepository(ContentPreferenceSource).findOneBy({ + referenceId: newId, sourceId: newId, userId: '1', }); - expect(member.role).toEqual(SourceMemberRoles.Admin); + expect(member?.flags.role).toEqual(SourceMemberRoles.Admin); const post = await con .getRepository(SharePost) .findOneBy({ sourceId: newId }); @@ -2278,6 +2480,17 @@ describe('mutation editSquad', () => { referralToken: 'rt2', role: SourceMemberRoles.Admin, }); + await con.getRepository(ContentPreferenceSource).save({ + sourceId: 's1', + referenceId: 's1', + userId: '1', + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'rt2', + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', + }); }); it('squad should have post moderation enabled after update', async () => { @@ -2455,9 +2668,14 @@ describe('mutation editSquad', () => { it(`should throw error if user is not the squad admin`, async () => { loggedUser = '1'; - await con - .getRepository(SourceMember) - .update({ userId: '1' }, { role: SourceMemberRoles.Member }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '1' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Member, + }), + }, + ); return testMutationErrorCode( client, { mutation: MUTATION, variables }, @@ -2760,9 +2978,14 @@ describe('mutation updateMemberRole', () => { it('should restrict moderator updating another member to a new role', async () => { loggedUser = '2'; - await con - .getRepository(SourceMember) - .update({ userId: '2' }, { role: SourceMemberRoles.Moderator }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), + }, + ); return testMutationErrorCode( client, { @@ -2800,9 +3023,14 @@ describe('mutation updateMemberRole', () => { it('should allow admin to promote a moderator to an admin', async () => { loggedUser = '1'; - await con - .getRepository(SourceMember) - .update({ userId: '2' }, { role: SourceMemberRoles.Moderator }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), + }, + ); const res = await client.mutate(MUTATION, { variables: { sourceId: 'a', @@ -2824,9 +3052,14 @@ describe('mutation updateMemberRole', () => { it('should allow admin to demote an admin to a moderator', async () => { loggedUser = '1'; - await con - .getRepository(SourceMember) - .update({ userId: '2' }, { role: SourceMemberRoles.Admin }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), + }, + ); const res = await client.mutate(MUTATION, { variables: { sourceId: 'a', @@ -2848,9 +3081,14 @@ describe('mutation updateMemberRole', () => { it('should allow admin to demote a moderator to a member', async () => { loggedUser = '1'; - await con - .getRepository(SourceMember) - .update({ userId: '2' }, { role: SourceMemberRoles.Moderator }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), + }, + ); const res = await client.mutate(MUTATION, { variables: { sourceId: 'a', @@ -2872,9 +3110,14 @@ describe('mutation updateMemberRole', () => { it('should allow admin to remove and block an admin', async () => { loggedUser = '1'; - await con - .getRepository(SourceMember) - .update({ userId: '2' }, { role: SourceMemberRoles.Admin }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Admin, + }), + }, + ); const res = await client.mutate(MUTATION, { variables: { sourceId: 'a', @@ -2896,9 +3139,14 @@ describe('mutation updateMemberRole', () => { it('should allow admin to remove and block a moderator', async () => { loggedUser = '1'; - await con - .getRepository(SourceMember) - .update({ userId: '2' }, { role: SourceMemberRoles.Moderator }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), + }, + ); const res = await client.mutate(MUTATION, { variables: { sourceId: 'a', @@ -2941,12 +3189,22 @@ describe('mutation updateMemberRole', () => { it('should restrict moderator to remove and block a moderator', async () => { loggedUser = '2'; - await con - .getRepository(SourceMember) - .update({ userId: '2' }, { role: SourceMemberRoles.Moderator }); - await con - .getRepository(SourceMember) - .update({ userId: '3' }, { role: SourceMemberRoles.Moderator }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), + }, + ); + await con.getRepository(ContentPreferenceSource).update( + { userId: '3' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), + }, + ); return testMutationErrorCode( client, { @@ -2963,9 +3221,14 @@ describe('mutation updateMemberRole', () => { it('should restrict moderator to remove and block an admin', async () => { loggedUser = '2'; - await con - .getRepository(SourceMember) - .update({ userId: '2' }, { role: SourceMemberRoles.Moderator }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), + }, + ); return testMutationErrorCode( client, { @@ -2982,9 +3245,14 @@ describe('mutation updateMemberRole', () => { it('should allow moderator to remove and block a member', async () => { loggedUser = '2'; - await con - .getRepository(SourceMember) - .update({ userId: '2' }, { role: SourceMemberRoles.Moderator }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), + }, + ); const res = await client.mutate(MUTATION, { variables: { sourceId: 'a', @@ -3072,9 +3340,14 @@ describe('mutation unblockMember', () => { it('should allow moderator to unblock a member', async () => { loggedUser = '2'; - await con - .getRepository(SourceMember) - .update({ userId: '2' }, { role: SourceMemberRoles.Moderator }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Moderator, + }), + }, + ); const res = await client.mutate(MUTATION, { variables: { sourceId: 'a', memberId: '3' }, }); @@ -3174,15 +3447,27 @@ describe('mutation leaveSource', () => { it('should leave squad even if the user is the admin', async () => { loggedUser = '1'; - await con - .getRepository(SourceMember) - .update({ userId: '1' }, { role: SourceMemberRoles.Admin }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '1' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Admin, + }), + }, + ); const res = await client.mutate(MUTATION, { variables }); expect(res.errors).toBeFalsy(); const sourceMembers = await con .getRepository(SourceMember) .countBy(variables); expect(sourceMembers).toEqual(0); + + const contentPreferences = await con + .getRepository(ContentPreferenceSource) + .countBy(variables); + expect(contentPreferences).toEqual(0); + + expect(sourceMembers).toEqual(0); }); }); @@ -3208,6 +3493,18 @@ describe('mutation deleteSource', () => { referralToken: 'rt2', role: SourceMemberRoles.Member, }); + await con.getRepository(ContentPreferenceSource).save({ + userId: '1', + referenceId: 's1', + sourceId: 's1', + feedId: '1', + status: ContentPreferenceStatus.Subscribed, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt2', + }, + type: ContentPreferenceType.Source, + }); }); const variables = { @@ -3235,9 +3532,14 @@ describe('mutation deleteSource', () => { it('should delete source and members', async () => { loggedUser = '1'; - await con - .getRepository(SourceMember) - .update({ userId: '1' }, { role: SourceMemberRoles.Admin }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '1' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Admin, + }), + }, + ); const res = await client.mutate(MUTATION, { variables }); expect(res.errors).toBeFalsy(); @@ -3245,6 +3547,12 @@ describe('mutation deleteSource', () => { .getRepository(SourceMember) .countBy(variables); expect(sourceMembers).toEqual(0); + + const contentPreferences = await con + .getRepository(ContentPreferenceSource) + .countBy(variables); + expect(contentPreferences).toEqual(0); + const source = await con.getRepository(SquadSource).countBy({ id: 's1' }); expect(source).toEqual(0); }); @@ -3275,6 +3583,18 @@ describe('mutation joinSource', () => { referralToken: 'rt2', role: SourceMemberRoles.Admin, }); + await con.getRepository(ContentPreferenceSource).save({ + userId: '2', + referenceId: 's1', + sourceId: 's1', + feedId: '2', + status: ContentPreferenceStatus.Subscribed, + flags: { + role: SourceMemberRoles.Admin, + referralToken: 'rt2', + }, + type: ContentPreferenceType.Source, + }); }); it('should not authorize when not logged in', () => @@ -3297,6 +3617,11 @@ describe('mutation joinSource', () => { sourceId: 's1', userId: '1', }); + await con.getRepository(ContentPreferenceSource).findOneByOrFail({ + userId: '1', + sourceId: 's1', + referenceId: 's1', + }); }); it('should add member to private squad with token', async () => { @@ -3314,6 +3639,11 @@ describe('mutation joinSource', () => { sourceId: 's1', userId: '1', }); + await con.getRepository(ContentPreferenceSource).findOneByOrFail({ + userId: '1', + sourceId: 's1', + referenceId: 's1', + }); const source = await con.getRepository(Source).findOneBy({ id: 's1' }); expect(source.active).toEqual(true); const preference = await con @@ -3355,6 +3685,14 @@ describe('mutation joinSource', () => { userId: '2', }); expect(member.role).toEqual(SourceMemberRoles.Admin); + const contentPreference = await con + .getRepository(ContentPreferenceSource) + .findOneByOrFail({ + userId: '2', + sourceId: 's1', + referenceId: 's1', + }); + expect(contentPreference.flags.role).toEqual(SourceMemberRoles.Admin); }); it('should throw error when joining private squad without token', async () => { @@ -3368,9 +3706,14 @@ describe('mutation joinSource', () => { it('should throw error when joining private squad when blocked', async () => { loggedUser = '2'; - await con - .getRepository(SourceMember) - .update({ userId: '2' }, { role: SourceMemberRoles.Blocked }); + await con.getRepository(ContentPreferenceSource).update( + { userId: '2' }, + { + flags: updateFlagsStatement({ + role: SourceMemberRoles.Blocked, + }), + }, + ); return testMutationErrorCode( client, { mutation: MUTATION, variables }, @@ -3427,11 +3770,17 @@ describe('mutation joinSource', () => { private: true, memberInviteRank: sourceRoleRank[SourceMemberRoles.Moderator], }); - await con.getRepository(SourceMember).save({ - sourceId: 's1', + await con.getRepository(ContentPreferenceSource).save({ userId: '2', - referralToken: 'rt2', - role: SourceMemberRoles.Member, + referenceId: 's1', + sourceId: 's1', + feedId: '2', + status: ContentPreferenceStatus.Subscribed, + flags: { + role: SourceMemberRoles.Member, + referralToken: 'rt2', + }, + type: ContentPreferenceType.Source, }); loggedUser = '1'; @@ -3478,34 +3827,54 @@ query Source($id: ID!) { const now = new Date(2022, 11, 19); - await con.getRepository(SourceMember).save([ + await con.getRepository(ContentPreferenceSource).save([ { - userId: '1', sourceId: 'c', - role: SourceMemberRoles.Admin, - referralToken: randomUUID(), + referenceId: 'c', + userId: '1', createdAt: new Date(now.getTime() + 0), + flags: { + role: SourceMemberRoles.Admin, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '1', }, { - userId: '2', sourceId: 'c', - role: SourceMemberRoles.Moderator, - referralToken: randomUUID(), + referenceId: 'c', + userId: '2', createdAt: new Date(now.getTime() + 1000), + flags: { + role: SourceMemberRoles.Moderator, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '2', }, { - userId: '3', sourceId: 'c', - role: SourceMemberRoles.Moderator, - referralToken: randomUUID(), + referenceId: 'c', + userId: '3', createdAt: new Date(now.getTime() + 2000), + flags: { + role: SourceMemberRoles.Moderator, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '3', }, { - userId: '4', sourceId: 'c', - role: SourceMemberRoles.Member, - referralToken: randomUUID(), + referenceId: 'c', + userId: '4', createdAt: new Date(now.getTime() + 3000), + flags: { + role: SourceMemberRoles.Member, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: '4', }, ]); }); @@ -3601,8 +3970,9 @@ describe('mutation hideSourceFeedPosts', () => { it('should throw when user is not a member', async () => { loggedUser = '1'; - await con.getRepository(SourceMember).delete({ + await con.getRepository(ContentPreferenceSource).delete({ sourceId: 's1', + referenceId: 's1', userId: '1', }); await testMutationErrorCode( @@ -3617,13 +3987,16 @@ describe('mutation hideSourceFeedPosts', () => { it('should throw when user is blocked', async () => { loggedUser = '1'; - await con.getRepository(SourceMember).update( + await con.getRepository(ContentPreferenceSource).update( { sourceId: 's1', + referenceId: 's1', userId: '1', }, { - role: SourceMemberRoles.Blocked, + flags: updateFlagsStatement({ + role: SourceMemberRoles.Blocked, + }), }, ); await testMutationErrorCode( @@ -3709,8 +4082,9 @@ describe('mutation showSourceFeedPosts', () => { it('should throw when user is not a member', async () => { loggedUser = '1'; - await con.getRepository(SourceMember).delete({ + await con.getRepository(ContentPreferenceSource).delete({ sourceId: 's1', + referenceId: 's1', userId: '1', }); await testMutationErrorCode( @@ -3725,13 +4099,16 @@ describe('mutation showSourceFeedPosts', () => { it('should throw when user is blocked', async () => { loggedUser = '1'; - await con.getRepository(SourceMember).update( + await con.getRepository(ContentPreferenceSource).update( { sourceId: 's1', + referenceId: 's1', userId: '1', }, { - role: SourceMemberRoles.Blocked, + flags: updateFlagsStatement({ + role: SourceMemberRoles.Blocked, + }), }, ); await testMutationErrorCode( @@ -3753,6 +4130,11 @@ describe('mutation showSourceFeedPosts', () => { expect(sourceMember).toBeTruthy(); expect(sourceMember?.flags.hideFeedPosts).toEqual(undefined); + let contentPreference = await con + .getRepository(ContentPreferenceSource) + .findOneBy({ userId: '1', referenceId: 's1' }); + expect(contentPreference!.flags.hideFeedPosts).toEqual(undefined); + await client.mutate(MUTATION, { variables: { sourceId: 's1' } }); sourceMember = await con.getRepository(SourceMember).findOneBy({ sourceId: 's1', @@ -3760,7 +4142,7 @@ describe('mutation showSourceFeedPosts', () => { }); expect(sourceMember?.flags.hideFeedPosts).toEqual(false); - const contentPreference = await con + contentPreference = await con .getRepository(ContentPreferenceSource) .findOneBy({ userId: '1', referenceId: 's1' }); expect(contentPreference!.flags.hideFeedPosts).toEqual(false); @@ -3817,8 +4199,9 @@ describe('mutation collapsePinnedPosts', () => { it('should throw when user is not a member', async () => { loggedUser = '1'; - await con.getRepository(SourceMember).delete({ + await con.getRepository(ContentPreferenceSource).delete({ sourceId: 's1', + referenceId: 's1', userId: '1', }); await testMutationErrorCode( @@ -3833,13 +4216,16 @@ describe('mutation collapsePinnedPosts', () => { it('should throw when user is blocked', async () => { loggedUser = '1'; - await con.getRepository(SourceMember).update( + await con.getRepository(ContentPreferenceSource).update( { sourceId: 's1', + referenceId: 's1', userId: '1', }, { - role: SourceMemberRoles.Blocked, + flags: updateFlagsStatement({ + role: SourceMemberRoles.Blocked, + }), }, ); await testMutationErrorCode( @@ -3861,6 +4247,11 @@ describe('mutation collapsePinnedPosts', () => { expect(sourceMember).toBeTruthy(); expect(sourceMember?.flags.collapsePinnedPosts).toEqual(undefined); + let contentPreference = await con + .getRepository(ContentPreferenceSource) + .findOneBy({ userId: '1', referenceId: 's1' }); + expect(contentPreference!.flags.collapsePinnedPosts).toEqual(undefined); + await client.mutate(MUTATION, { variables: { sourceId: 's1' } }); sourceMember = await con.getRepository(SourceMember).findOneBy({ sourceId: 's1', @@ -3868,7 +4259,7 @@ describe('mutation collapsePinnedPosts', () => { }); expect(sourceMember?.flags.collapsePinnedPosts).toEqual(true); - const contentPreference = await con + contentPreference = await con .getRepository(ContentPreferenceSource) .findOneBy({ userId: '1', referenceId: 's1' }); expect(contentPreference!.flags.collapsePinnedPosts).toEqual(true); @@ -3925,8 +4316,9 @@ describe('mutation expandPinnedPosts', () => { it('should throw when user is not a member', async () => { loggedUser = '1'; - await con.getRepository(SourceMember).delete({ + await con.getRepository(ContentPreferenceSource).delete({ sourceId: 's1', + referenceId: 's1', userId: '1', }); await testMutationErrorCode( @@ -3941,13 +4333,16 @@ describe('mutation expandPinnedPosts', () => { it('should throw when user is blocked', async () => { loggedUser = '1'; - await con.getRepository(SourceMember).update( + await con.getRepository(ContentPreferenceSource).update( { sourceId: 's1', + referenceId: 's1', userId: '1', }, { - role: SourceMemberRoles.Blocked, + flags: updateFlagsStatement({ + role: SourceMemberRoles.Blocked, + }), }, ); await testMutationErrorCode( @@ -3969,6 +4364,11 @@ describe('mutation expandPinnedPosts', () => { expect(sourceMember).toBeTruthy(); expect(sourceMember?.flags.collapsePinnedPosts).toEqual(undefined); + let contentPreference = await con + .getRepository(ContentPreferenceSource) + .findOneBy({ userId: '1', referenceId: 's1' }); + expect(contentPreference!.flags.collapsePinnedPosts).toEqual(undefined); + await client.mutate(MUTATION, { variables: { sourceId: 's1' } }); sourceMember = await con.getRepository(SourceMember).findOneBy({ sourceId: 's1', @@ -3976,7 +4376,7 @@ describe('mutation expandPinnedPosts', () => { }); expect(sourceMember?.flags.collapsePinnedPosts).toEqual(false); - const contentPreference = await con + contentPreference = await con .getRepository(ContentPreferenceSource) .findOneBy({ userId: '1', referenceId: 's1' }); expect(contentPreference!.flags.collapsePinnedPosts).toEqual(false); @@ -3997,8 +4397,8 @@ describe('SourceMember flags field', () => { it('should return all the public flags for source member', async () => { loggedUser = '1'; - await con.getRepository(SourceMember).update( - { userId: '1', sourceId: 'a' }, + await con.getRepository(ContentPreferenceSource).update( + { userId: '1', referenceId: 'a' }, { flags: updateFlagsStatement({ hideFeedPosts: true, diff --git a/src/graphorm/index.ts b/src/graphorm/index.ts index 7d3d2a119..7a0c56254 100644 --- a/src/graphorm/index.ts +++ b/src/graphorm/index.ts @@ -538,20 +538,24 @@ const obj = new GraphORM({ }, roleRank: { rawSelect: true, - select: ` + select: (ctx, alias) => { + return ` (CASE ${sourceRoleRankKeys .map( (role) => - `WHEN flags->>'role' = '${role}' THEN ${sourceRoleRank[role as keyof typeof sourceRoleRank]}`, + `WHEN ${alias}.flags->>'role' = '${role}' THEN ${sourceRoleRank[role as keyof typeof sourceRoleRank]}`, ) .join(' ')} ELSE 0 END) - `, + `; + }, }, referralToken: { rawSelect: true, - select: `flags->>'referralToken'`, + select: (ctx, alias) => { + return `${alias}.flags->>'referralToken'`; + }, transform: (value: string, ctx: Context, parent) => { const member = parent as ContentPreferenceSource; @@ -560,7 +564,9 @@ const obj = new GraphORM({ }, role: { rawSelect: true, - select: `COALESCE(flags->>'role', '${SourceMemberRoles.Member}')`, + select: (ctx, alias) => { + return `COALESCE(${alias}.flags->>'role', '${SourceMemberRoles.Member}')`; + }, }, flags: { jsonType: true, diff --git a/src/schema/sources.ts b/src/schema/sources.ts index 643a9e2ea..7e6aeaf53 100644 --- a/src/schema/sources.ts +++ b/src/schema/sources.ts @@ -1772,6 +1772,18 @@ export const resolvers: IResolvers = traceResolvers< graphorm.mappings.SourceMember.fields?.roleRank.select, 'DESC', ); + } else if ( + typeof graphorm.mappings!.SourceMember!.fields!.roleRank.select === + 'function' + ) { + queryBuilder = queryBuilder.addOrderBy( + graphorm.mappings!.SourceMember!.fields!.roleRank.select( + ctx, + alias, + queryBuilder, + ) as string, + 'DESC', + ); } queryBuilder = queryBuilder.addOrderBy( @@ -1801,6 +1813,13 @@ export const resolvers: IResolvers = traceResolvers< queryBuilder = queryBuilder.andWhere( `${graphorm.mappings.SourceMember.fields.roleRank.select} >= 0`, ); + } else if ( + typeof graphorm.mappings!.SourceMember!.fields!.roleRank.select === + 'function' + ) { + queryBuilder = queryBuilder.andWhere( + `${graphorm.mappings!.SourceMember!.fields!.roleRank.select(ctx, alias, queryBuilder)} >= 0`, + ); } return queryBuilder; }, @@ -1830,6 +1849,13 @@ export const resolvers: IResolvers = traceResolvers< queryBuilder = queryBuilder.andWhere( `${graphorm.mappings.SourceMember.fields.roleRank.select} >= 0`, ); + } else if ( + typeof graphorm.mappings!.SourceMember!.fields!.roleRank.select === + 'function' + ) { + queryBuilder = queryBuilder.andWhere( + `${graphorm.mappings!.SourceMember!.fields!.roleRank.select(ctx, alias, queryBuilder)} >= 0`, + ); } queryBuilder = queryBuilder.addOrderBy( From c4b7620712aedda0848a8f72a4dea04e06837227 Mon Sep 17 00:00:00 2001 From: capJavert Date: Mon, 18 Nov 2024 15:12:15 +0100 Subject: [PATCH 7/9] feat: debezium config --- .infra/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.infra/application.properties b/.infra/application.properties index 257255908..367328d6d 100644 --- a/.infra/application.properties +++ b/.infra/application.properties @@ -8,7 +8,7 @@ debezium.source.database.user=%database_user% debezium.source.database.password=%database_pass% debezium.source.database.dbname=%database_dbname% debezium.source.database.server.name=api -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 +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.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.content_preference debezium.source.column.exclude.list=public.post.tsv,public.post.placeholder,public.source.flags,public.user_top_reader.image debezium.source.skip.messages.without.change=true debezium.source.plugin.name=pgoutput From 9de27e17051b02d3ce75e274bf9a9300d5146b06 Mon Sep 17 00:00:00 2001 From: capJavert Date: Thu, 28 Nov 2024 17:38:05 +0100 Subject: [PATCH 8/9] feat: adjust based on blocked status feat: do not show status blocked in member lists feat: do not send notifications for status blocked members feat: still show member is part of source if status was blocked (since new UI is missing this is temp nuances) --- __tests__/__snapshots__/sources.ts.snap | 27 +++++++++ __tests__/sources.ts | 12 ++++ __tests__/workers/cdc/primary.ts | 22 ++++++- src/common/feedGenerator.ts | 1 + src/common/users.ts | 11 +++- src/graphorm/index.ts | 4 +- src/routes/boot.ts | 4 ++ src/schema/comments.ts | 8 ++- src/schema/sources.ts | 31 ++++++---- src/workers/cdc/primary.ts | 60 +++++++++++-------- src/workers/notifications/postAdded.ts | 4 ++ .../notifications/squadFeaturedUpdated.ts | 4 ++ src/workers/notifications/utils.ts | 4 ++ 13 files changed, 150 insertions(+), 42 deletions(-) diff --git a/__tests__/__snapshots__/sources.ts.snap b/__tests__/__snapshots__/sources.ts.snap index bf33d08d3..e0af15147 100644 --- a/__tests__/__snapshots__/sources.ts.snap +++ b/__tests__/__snapshots__/sources.ts.snap @@ -341,3 +341,30 @@ Object { }, } `; + +exports[`query sourceMembers should return source members of source without members that blocked squad 1`] = ` +Object { + "sourceMembers": Object { + "edges": Array [ + Object { + "node": Object { + "role": "admin", + "roleRank": 10, + "source": Object { + "id": "a", + }, + "user": Object { + "id": "1", + "name": "Ido", + "username": "idoshamun", + }, + }, + }, + ], + "pageInfo": Object { + "endCursor": "YXJyYXljb25uZWN0aW9uOjA=", + "hasNextPage": false, + }, + }, +} +`; diff --git a/__tests__/sources.ts b/__tests__/sources.ts index d2ceb8526..951f47478 100644 --- a/__tests__/sources.ts +++ b/__tests__/sources.ts @@ -1672,6 +1672,18 @@ describe('query sourceMembers', () => { expect(res.data).toMatchSnapshot(); }); + it('should return source members of source without members that blocked squad', async () => { + await con.getRepository(ContentPreferenceSource).update( + { userId: '2' }, + { + status: ContentPreferenceStatus.Blocked, + }, + ); + const res = await client.query(QUERY, { variables: { id: 'a' } }); + expect(res.errors).toBeFalsy(); + expect(res.data).toMatchSnapshot(); + }); + it('should return source members without blocked members and based on query', async () => { const res = await client.query(QUERY, { variables: { id: 'a', query: 'i' }, diff --git a/__tests__/workers/cdc/primary.ts b/__tests__/workers/cdc/primary.ts index b4a53bc6b..8ccbb48a2 100644 --- a/__tests__/workers/cdc/primary.ts +++ b/__tests__/workers/cdc/primary.ts @@ -147,7 +147,10 @@ import { SourcePostModerationStatus, } from '../../../src/entity/SourcePostModeration'; import { ContentPreferenceSource } from '../../../src/entity/contentPreference/ContentPreferenceSource'; -import { ContentPreferenceStatus } from '../../../src/entity/contentPreference/types'; +import { + ContentPreferenceStatus, + ContentPreferenceType, +} from '../../../src/entity/contentPreference/types'; import { NotificationType } from '../../../src/notifications/common'; jest.mock('../../../src/common', () => ({ @@ -1976,6 +1979,7 @@ describe('source member', () => { }, status: ContentPreferenceStatus.Subscribed, feedId: '1', + type: ContentPreferenceType.Source, }; beforeEach(async () => { @@ -2005,6 +2009,22 @@ describe('source member', () => { ).toEqual([base]); }); + it('should not notify on new source member if status is blocked', async () => { + await expectSuccessfulBackground( + worker, + mockChangeMessage({ + after: { + ...base, + status: ContentPreferenceStatus.Blocked, + }, + before: null, + op: 'c', + table: 'content_preference', + }), + ); + expect(notifyMemberJoinedSource).toHaveBeenCalledTimes(0); + }); + it('should notify when role changed', async () => { const after: ChangeObject = { ...base, diff --git a/src/common/feedGenerator.ts b/src/common/feedGenerator.ts index c77b64e9b..971589f5f 100644 --- a/src/common/feedGenerator.ts +++ b/src/common/feedGenerator.ts @@ -148,6 +148,7 @@ const getRawFiltersData = async ( `t."sourceId" = source.id AND source.type = '${SourceType.Squad}'`, ) .where(`t.type = '${ContentPreferenceType.Source}'`) + .andWhere(`t.status != '${ContentPreferenceStatus.Blocked}'`) .andWhere('"userId" = $2'), ), ]; diff --git a/src/common/users.ts b/src/common/users.ts index ae3ea68bb..55f84f8ce 100644 --- a/src/common/users.ts +++ b/src/common/users.ts @@ -21,6 +21,7 @@ import { logger } from '../logger'; import type { GQLKeyword } from '../schema/keywords'; import type { GQLUser } from '../schema/users'; import { ContentPreferenceSource } from '../entity/contentPreference/ContentPreferenceSource'; +import { ContentPreferenceStatus } from '../entity/contentPreference/types'; export interface User { id: string; @@ -170,7 +171,10 @@ export const getRecentMentionsIds = async ( 'cps', 'cps."userId" = cm."mentionedUserId"', ) - .andWhere('cps."referenceId" = :sourceId', { sourceId }); + .andWhere('cps."referenceId" = :sourceId', { sourceId }) + .andWhere(`cps.status != :status`, { + status: ContentPreferenceStatus.Blocked, + }); } if (query) { @@ -212,7 +216,10 @@ export const getUserIdsByNameOrUsername = async ( if (sourceId) { queryBuilder = queryBuilder .innerJoin(ContentPreferenceSource, 'cps', 'id = cps."userId"') - .andWhere('cps."referenceId" = :sourceId', { sourceId }); + .andWhere('cps."referenceId" = :sourceId', { sourceId }) + .andWhere(`cps.status != :status`, { + status: ContentPreferenceStatus.Blocked, + }); } if (excludeIds?.length) { diff --git a/src/graphorm/index.ts b/src/graphorm/index.ts index 7a0c56254..278adb664 100644 --- a/src/graphorm/index.ts +++ b/src/graphorm/index.ts @@ -478,7 +478,7 @@ const obj = new GraphORM({ customRelation: (ctx, parentAlias, childAlias, qb): QueryBuilder => qb .where(`${childAlias}."userId" = :userId`, { userId: ctx.userId }) - .andWhere(`${childAlias}."sourceId" = "${parentAlias}".id`), + .andWhere(`${childAlias}."referenceId" = "${parentAlias}".id`), }, }, privilegedMembers: { @@ -518,7 +518,7 @@ const obj = new GraphORM({ const query = qb .select('array["memberPostingRank", "memberInviteRank"]') .from(Source, 'postingSquad') - .where(`postingSquad.id = ${alias}."sourceId"`); + .where(`postingSquad.id = ${alias}."referenceId"`); return `${query.getQuery()}`; }, transform: (value: [number, number], ctx: Context, parent) => { diff --git a/src/routes/boot.ts b/src/routes/boot.ts index f7dac5456..73210aa2c 100644 --- a/src/routes/boot.ts +++ b/src/routes/boot.ts @@ -67,6 +67,7 @@ import { ContentPreferenceSource, ContentPreferenceSourceFlags, } from '../entity/contentPreference/ContentPreferenceSource'; +import { ContentPreferenceStatus } from '../entity/contentPreference/types'; export type BootSquadSource = Omit & { permalink: string; @@ -206,6 +207,9 @@ const getSquads = async ( .andWhere(`cps.flags->>'role' != :role`, { role: SourceMemberRoles.Blocked, }) + .andWhere(`cps.status != :status`, { + status: ContentPreferenceStatus.Blocked, + }) .orderBy('LOWER(s.name)', 'ASC') .getRawMany< GQLSource & { diff --git a/src/schema/comments.ts b/src/schema/comments.ts index 586837ece..6d2824e17 100644 --- a/src/schema/comments.ts +++ b/src/schema/comments.ts @@ -52,6 +52,7 @@ import { reportComment } from '../common/reporting'; import { ReportReason } from '../entity/common'; import { toGQLEnum } from '../common/utils'; import { ContentPreferenceSource } from '../entity/contentPreference/ContentPreferenceSource'; +import { ContentPreferenceStatus } from '../entity/contentPreference/types'; export interface GQLComment { id: string; @@ -481,7 +482,12 @@ export const getMentions = async ( return repo .createQueryBuilder('u') .select('u.id, u.username') - .innerJoin(ContentPreferenceSource, 'cps', 'u.id = cps."userId"') + .innerJoin( + ContentPreferenceSource, + 'cps', + 'u.id = cps."userId" AND cps.status != :status', + { status: ContentPreferenceStatus.Blocked }, + ) .where('cps."referenceId" = :sourceId', { sourceId }) .andWhere('u.username IN (:...usernames)', { usernames: result }) .andWhere('u.id != :id', { id: userId }) diff --git a/src/schema/sources.ts b/src/schema/sources.ts index 1e93cd390..750b045f6 100644 --- a/src/schema/sources.ts +++ b/src/schema/sources.ts @@ -1788,12 +1788,13 @@ export const resolvers: IResolvers = traceResolvers< await ensureSourcePermissions(ctx, sourceId, permission); return paginateSourceMembers( (queryBuilder, alias) => { - queryBuilder = queryBuilder.andWhere( - `${alias}."referenceId" = :source`, - { + queryBuilder = queryBuilder + .andWhere(`${alias}."referenceId" = :source`, { source: sourceId, - }, - ); + }) + .andWhere(`${alias}.status != :memberContentPreferenceStatus`, { + memberContentPreferenceStatus: ContentPreferenceStatus.Blocked, + }); if ( typeof graphorm.mappings?.SourceMember.fields?.roleRank.select === @@ -1869,9 +1870,13 @@ export const resolvers: IResolvers = traceResolvers< return paginateSourceMembers( (queryBuilder, alias) => { - queryBuilder = queryBuilder.andWhere(`${alias}."userId" = :userId`, { - userId: ctx.userId, - }); + queryBuilder = queryBuilder + .andWhere(`${alias}."userId" = :userId`, { + userId: ctx.userId, + }) + .andWhere(`${alias}.status != :memberContentPreferenceStatus`, { + memberContentPreferenceStatus: ContentPreferenceStatus.Blocked, + }); if ( typeof graphorm.mappings?.SourceMember.fields?.roleRank.select === @@ -1916,9 +1921,13 @@ export const resolvers: IResolvers = traceResolvers< ): Promise> => { return paginateSourceMembers( (queryBuilder, alias) => { - queryBuilder = queryBuilder.andWhere(`${alias}."userId" = :userId`, { - userId, - }); + queryBuilder = queryBuilder + .andWhere(`${alias}."userId" = :userId`, { + userId, + }) + .andWhere(`${alias}.status != :memberContentPreferenceStatus`, { + memberContentPreferenceStatus: ContentPreferenceStatus.Blocked, + }); if ( typeof graphorm.mappings?.SourceMember.fields?.roleRank.select === diff --git a/src/workers/cdc/primary.ts b/src/workers/cdc/primary.ts index 2cb470bb5..64875216e 100644 --- a/src/workers/cdc/primary.ts +++ b/src/workers/cdc/primary.ts @@ -127,6 +127,10 @@ import { import { ContentPreferenceSource } from '../../entity/contentPreference/ContentPreferenceSource'; import { ContentPreference } from '../../entity/contentPreference/ContentPreference'; import { cleanupSourcePostModerationNotifications } from '../../notifications/common'; +import { + ContentPreferenceStatus, + ContentPreferenceType, +} from '../../entity/contentPreference/types'; const isFreeformPostLongEnough = ( freeform: ChangeMessage, @@ -847,32 +851,38 @@ const onSourceMemberChange = async ( logger: FastifyBaseLogger, data: ChangeMessage, ) => { - const sourceId = - data.payload.after!.sourceId || data.payload.before!.sourceId; - - const source = await con - .getRepository(Source) - .createQueryBuilder() - .select('type') - .where({ - id: sourceId, - }) - .getRawOne>(); - - if (source?.type !== SourceType.Squad) { - return; - } + const contentPreferenceType = data.payload.after!.type; - if (data.payload.op === 'c') { - await notifyMemberJoinedSource(logger, data.payload.after!); - } - if (data.payload.op === 'u') { - if (data.payload.before!.flags.role !== data.payload.after!.flags.role) { - await notifySourceMemberRoleChanged( - logger, - data.payload.before!.flags.role!, - data.payload.after!, - ); + if (contentPreferenceType === ContentPreferenceType.Source) { + const sourceId = data.payload.after!.sourceId; + + const source = await con + .getRepository(Source) + .createQueryBuilder() + .select('type') + .where({ + id: sourceId, + }) + .getRawOne>(); + + if (source?.type !== SourceType.Squad) { + return; + } + + const isMemberBlockingSource = + data.payload.after?.status === ContentPreferenceStatus.Blocked; + + if (data.payload.op === 'c' && !isMemberBlockingSource) { + await notifyMemberJoinedSource(logger, data.payload.after!); + } + if (data.payload.op === 'u') { + if (data.payload.before!.flags.role !== data.payload.after!.flags.role) { + await notifySourceMemberRoleChanged( + logger, + data.payload.before!.flags.role!, + data.payload.after!, + ); + } } } }; diff --git a/src/workers/notifications/postAdded.ts b/src/workers/notifications/postAdded.ts index 283018575..2fbda3417 100644 --- a/src/workers/notifications/postAdded.ts +++ b/src/workers/notifications/postAdded.ts @@ -23,6 +23,7 @@ import { ChangeObject } from '../../types'; import { buildPostContext, getSubscribedMembers } from './utils'; import { SourceMemberRoles } from '../../roles'; import { insertOrIgnoreAction } from '../../schema/actions'; +import { ContentPreferenceStatus } from '../../entity/contentPreference/types'; interface Data { post: ChangeObject; @@ -92,6 +93,9 @@ const worker: NotificationWorker = { .andWhere(`${qb.alias}."referenceId" = :sourceId`, { sourceId: source.id, }) + .andWhere(`status != :status`, { + status: ContentPreferenceStatus.Blocked, + }) .andWhere(` ${qb.alias}.flags->>'role' != :role`, { role: SourceMemberRoles.Blocked, }), diff --git a/src/workers/notifications/squadFeaturedUpdated.ts b/src/workers/notifications/squadFeaturedUpdated.ts index a2c8ce3ba..233c2157c 100644 --- a/src/workers/notifications/squadFeaturedUpdated.ts +++ b/src/workers/notifications/squadFeaturedUpdated.ts @@ -3,6 +3,7 @@ import { generateTypedNotificationWorker } from './worker'; import { NotificationSourceContext } from '../../notifications'; import { SourceMemberRoles } from '../../roles'; import { ContentPreferenceSource } from '../../entity/contentPreference/ContentPreferenceSource'; +import { ContentPreferenceStatus } from '../../entity/contentPreference/types'; const toNotify = [SourceMemberRoles.Admin, SourceMemberRoles.Moderator]; @@ -20,6 +21,9 @@ const worker = generateTypedNotificationWorker<'api.v1.squad-featured-updated'>( .select('"userId"') .where('"referenceId" = :sourceId', { sourceId: squad.id }) .andWhere(`flags->>'role' IN (:...roles)`, { roles: toNotify }) + .andWhere(`status != :status`, { + status: ContentPreferenceStatus.Blocked, + }) .getRawMany>(); if (!users.length) { diff --git a/src/workers/notifications/utils.ts b/src/workers/notifications/utils.ts index 59374155e..ead16f29c 100644 --- a/src/workers/notifications/utils.ts +++ b/src/workers/notifications/utils.ts @@ -27,6 +27,7 @@ import { insertOrIgnoreAction } from '../../schema/actions'; import { SourcePostModeration } from '../../entity/SourcePostModeration'; import { ChangeObject } from '../../types'; import { ContentPreferenceSource } from '../../entity/contentPreference/ContentPreferenceSource'; +import { ContentPreferenceStatus } from '../../entity/contentPreference/types'; export const uniquePostOwners = ( post: Pick, @@ -164,6 +165,9 @@ export async function articleNewCommentHandler( .andWhere(`${qb.alias}."referenceId" = :sourceId`, { sourceId: source.id, }) + .andWhere(`status != :status`, { + status: ContentPreferenceStatus.Blocked, + }) .andWhere(` ${qb.alias}.flags->>'role' != :role`, { role: SourceMemberRoles.Blocked, }), From 9bb570c02da21cefdf0db928194188320549071a Mon Sep 17 00:00:00 2001 From: capJavert Date: Thu, 28 Nov 2024 17:54:42 +0100 Subject: [PATCH 9/9] refactor: plus member to content preference --- .../workers/userUpdatedPlusSubscription.ts | 87 ++++++++++++------- src/workers/userUpdatedPlusSubscription.ts | 10 ++- 2 files changed, 63 insertions(+), 34 deletions(-) diff --git a/__tests__/workers/userUpdatedPlusSubscription.ts b/__tests__/workers/userUpdatedPlusSubscription.ts index 70cc05e43..e76268e96 100644 --- a/__tests__/workers/userUpdatedPlusSubscription.ts +++ b/__tests__/workers/userUpdatedPlusSubscription.ts @@ -6,10 +6,12 @@ import { User, Source, Feed } from '../../src/entity'; import { ghostUser, PubSubSchema } from '../../src/common'; import { typedWorkers } from '../../src/workers'; import createOrGetConnection from '../../src/db'; -import { DataSource } from 'typeorm'; +import { DataSource, Not } from 'typeorm'; import { usersFixture } from '../fixture/user'; import { SourceMemberRoles } from '../../src/roles'; import { randomUUID } from 'crypto'; +import { ContentPreferenceSource } from '../../src/entity/contentPreference/ContentPreferenceSource'; +import { ContentPreferenceStatus } from '../../src/entity/contentPreference/types'; const PLUS_MEMBER_SQUAD_ID = '05862288-bace-4723-9218-d30fab6ae96d'; @@ -80,13 +82,16 @@ describe('userUpdatedPlusSubscription', () => { newProfile: newBase, user: base, } as unknown as PubSubSchema['user-updated']); - const sourceMember = await con.getRepository('SourceMember').findOneBy({ - sourceId: PLUS_MEMBER_SQUAD_ID, - userId: newBase.id, - }); - expect(sourceMember.role).toEqual(SourceMemberRoles.Member); - expect(sourceMember.sourceId).toEqual(PLUS_MEMBER_SQUAD_ID); - expect(sourceMember.userId).toEqual(newBase.id); + const sourceMember = await con + .getRepository(ContentPreferenceSource) + .findOneBy({ + referenceId: PLUS_MEMBER_SQUAD_ID, + userId: newBase.id, + status: Not(ContentPreferenceStatus.Blocked), + }); + expect(sourceMember?.flags.role).toEqual(SourceMemberRoles.Member); + expect(sourceMember?.referenceId).toEqual(PLUS_MEMBER_SQUAD_ID); + expect(sourceMember?.userId).toEqual(newBase.id); }); it('should not add user if user is ghost user', async () => { @@ -95,10 +100,13 @@ describe('userUpdatedPlusSubscription', () => { newProfile: before, user: before, } as unknown as PubSubSchema['user-updated']); - const sourceMember = await con.getRepository('SourceMember').findOneBy({ - sourceId: PLUS_MEMBER_SQUAD_ID, - userId: base.id, - }); + const sourceMember = await con + .getRepository(ContentPreferenceSource) + .findOneBy({ + referenceId: PLUS_MEMBER_SQUAD_ID, + userId: base.id, + status: Not(ContentPreferenceStatus.Blocked), + }); expect(sourceMember).toEqual(null); }); @@ -108,10 +116,13 @@ describe('userUpdatedPlusSubscription', () => { newProfile: before, user: before, } as unknown as PubSubSchema['user-updated']); - const sourceMember = await con.getRepository('SourceMember').findOneBy({ - sourceId: PLUS_MEMBER_SQUAD_ID, - userId: base.id, - }); + const sourceMember = await con + .getRepository(ContentPreferenceSource) + .findOneBy({ + referenceId: PLUS_MEMBER_SQUAD_ID, + userId: base.id, + status: Not(ContentPreferenceStatus.Blocked), + }); expect(sourceMember).toEqual(null); }); @@ -121,10 +132,13 @@ describe('userUpdatedPlusSubscription', () => { newProfile: before, user: before, } as unknown as PubSubSchema['user-updated']); - const sourceMember = await con.getRepository('SourceMember').findOneBy({ - sourceId: PLUS_MEMBER_SQUAD_ID, - userId: base.id, - }); + const sourceMember = await con + .getRepository(ContentPreferenceSource) + .findOneBy({ + referenceId: PLUS_MEMBER_SQUAD_ID, + userId: base.id, + status: Not(ContentPreferenceStatus.Blocked), + }); expect(sourceMember).toEqual(null); }); @@ -137,19 +151,27 @@ describe('userUpdatedPlusSubscription', () => { newProfile: before, user: before, } as unknown as PubSubSchema['user-updated']); - const sourceMember = await con.getRepository('SourceMember').findOneBy({ - sourceId: PLUS_MEMBER_SQUAD_ID, - userId: base.id, - }); + const sourceMember = await con + .getRepository(ContentPreferenceSource) + .findOneBy({ + referenceId: PLUS_MEMBER_SQUAD_ID, + userId: base.id, + status: Not(ContentPreferenceStatus.Blocked), + }); expect(sourceMember).toEqual(null); }); it('should remove user from squad', async () => { - await con.getRepository('SourceMember').save({ + await con.getRepository(ContentPreferenceSource).save({ sourceId: PLUS_MEMBER_SQUAD_ID, + referenceId: PLUS_MEMBER_SQUAD_ID, userId: base.id, - role: SourceMemberRoles.Member, - referralToken: new randomUUID(), + flags: { + role: SourceMemberRoles.Member, + referralToken: randomUUID(), + }, + status: ContentPreferenceStatus.Subscribed, + feedId: base.id, }); const oldBase = { ...base, @@ -160,10 +182,13 @@ describe('userUpdatedPlusSubscription', () => { user: oldBase, } as unknown as PubSubSchema['user-updated']); - const sourceMember = await con.getRepository('SourceMember').findOneBy({ - sourceId: PLUS_MEMBER_SQUAD_ID, - userId: base.id, - }); + const sourceMember = await con + .getRepository(ContentPreferenceSource) + .findOneBy({ + referenceId: PLUS_MEMBER_SQUAD_ID, + userId: base.id, + status: Not(ContentPreferenceStatus.Blocked), + }); expect(sourceMember).toEqual(null); }); }); diff --git a/src/workers/userUpdatedPlusSubscription.ts b/src/workers/userUpdatedPlusSubscription.ts index 845298338..5873cccad 100644 --- a/src/workers/userUpdatedPlusSubscription.ts +++ b/src/workers/userUpdatedPlusSubscription.ts @@ -2,7 +2,10 @@ import { ghostUser } from '../common'; import { TypedWorker } from './worker'; import { addNewSourceMember, removeSourceMember } from '../schema/sources'; import { SourceMemberRoles } from '../roles'; -import { SourceMember, User } from '../entity'; +import { User } from '../entity'; +import { ContentPreferenceSource } from '../entity/contentPreference/ContentPreferenceSource'; +import { Not } from 'typeorm'; +import { ContentPreferenceStatus } from '../entity/contentPreference/types'; const PLUS_MEMBER_SQUAD_ID = '05862288-bace-4723-9218-d30fab6ae96d'; const worker: TypedWorker<'user-updated'> = { @@ -37,10 +40,11 @@ const worker: TypedWorker<'user-updated'> = { log.info({ userId: user.id }, 'removed user from plus member squad'); } else { // Started being plus member add them - const check = await con.getRepository(SourceMember).findOne({ + const check = await con.getRepository(ContentPreferenceSource).findOne({ where: { userId: user.id, - sourceId: PLUS_MEMBER_SQUAD_ID, + referenceId: PLUS_MEMBER_SQUAD_ID, + status: Not(ContentPreferenceStatus.Blocked), }, }); if (check) {