remove duplicate rss subscriptions by user and url; create a unique constraint

This commit is contained in:
Hongbo Wu
2023-10-16 17:35:29 +08:00
parent e54c1c81a1
commit 1b2d93e118
17 changed files with 70 additions and 61 deletions

View File

@ -1,6 +1,6 @@
import { In } from 'typeorm'
import { appDataSource } from '../data_source'
import { User } from './../entity/user'
import { StatusType, User } from './../entity/user'
const TOP_USERS = [
'jacksonh',
@ -16,7 +16,7 @@ export const MAX_RECORDS_LIMIT = 1000
export const userRepository = appDataSource.getRepository(User).extend({
findById(id: string) {
return this.findOneBy({ id })
return this.findOneBy({ id, status: StatusType.Active })
},
findByEmail(email: string) {

View File

@ -33,14 +33,14 @@ export const uploadImportFileResolver = authorized<
UploadImportFileSuccess,
UploadImportFileError,
MutationUploadImportFileArgs
>(async (_, { type, contentType }, { claims: { uid }, log }) => {
>(async (_, { type, contentType }, { uid }) => {
if (!VALID_CONTENT_TYPES.includes(contentType)) {
return {
errorCodes: [UploadImportFileErrorCode.BadRequest],
}
}
const user = await userRepository.findOneBy({ id: uid })
const user = await userRepository.findById(uid)
if (!user) {
return {
errorCodes: [UploadImportFileErrorCode.Unauthorized],

View File

@ -48,9 +48,7 @@ export const createGroupResolver = authorized<
MutationCreateGroupArgs
>(async (_, { input }, { uid, log }) => {
try {
const userData = await userRepository.findOneBy({
id: uid,
})
const userData = await userRepository.findById(uid)
if (!userData) {
return {
errorCodes: [CreateGroupErrorCode.Unauthorized],
@ -107,9 +105,7 @@ export const createGroupResolver = authorized<
export const groupsResolver = authorized<GroupsSuccess, GroupsError>(
async (_, __, { uid, log }) => {
try {
const user = await userRepository.findOneBy({
id: uid,
})
const user = await userRepository.findById(uid)
if (!user) {
return {
errorCodes: [GroupsErrorCode.Unauthorized],

View File

@ -18,9 +18,9 @@ export const savePageResolver = authorized<
SaveSuccess,
SaveError,
MutationSavePageArgs
>(async (_, { input }, ctx) => {
>(async (_, { input }, { uid }) => {
analytics.track({
userId: ctx.uid,
userId: uid,
event: 'link_saved',
properties: {
url: input.url,
@ -30,9 +30,7 @@ export const savePageResolver = authorized<
},
})
const user = await userRepository.findOneBy({
id: ctx.uid,
})
const user = await userRepository.findById(uid)
if (!user) {
return { errorCodes: [SaveErrorCode.Unauthorized] }
}
@ -44,11 +42,7 @@ export const saveUrlResolver = authorized<
SaveSuccess,
SaveError,
MutationSaveUrlArgs
>(async (_, { input }, ctx) => {
const {
claims: { uid },
} = ctx
>(async (_, { input }, { uid }) => {
analytics.track({
userId: uid,
event: 'link_saved',
@ -60,9 +54,7 @@ export const saveUrlResolver = authorized<
},
})
const user = await userRepository.findOneBy({
id: uid,
})
const user = await userRepository.findById(uid)
if (!user) {
return { errorCodes: [SaveErrorCode.Unauthorized] }
}
@ -74,9 +66,9 @@ export const saveFileResolver = authorized<
SaveSuccess,
SaveError,
MutationSaveFileArgs
>(async (_, { input }, ctx) => {
>(async (_, { input }, { uid }) => {
analytics.track({
userId: ctx.uid,
userId: uid,
event: 'link_saved',
properties: {
url: input.url,
@ -86,9 +78,7 @@ export const saveFileResolver = authorized<
},
})
const user = await userRepository.findOneBy({
id: ctx.uid,
})
const user = await userRepository.findById(uid)
if (!user) {
return { errorCodes: [SaveErrorCode.Unauthorized] }
}

View File

@ -14,11 +14,9 @@ const INSTALL_INSTRUCTIONS_EMAIL_TEMPLATE_ID =
export const sendInstallInstructionsResolver = authorized<
SendInstallInstructionsSuccess,
SendInstallInstructionsError
>(async (_parent, _args, { claims, log }) => {
>(async (_parent, _args, { uid, log }) => {
try {
const user = await userRepository.findOneBy({
id: claims.uid,
})
const user = await userRepository.findById(uid)
if (!user) {
return { errorCodes: [SendInstallInstructionsErrorCode.Unauthorized] }

View File

@ -89,7 +89,8 @@ export const subscriptionsResolver = authorized<
}
const subscriptions = await queryBuilder
.orderBy(`subscription.${sortBy}`, sortOrder, 'NULLS LAST')
.orderBy('subscription.status', 'ASC')
.addOrderBy(`subscription.${sortBy}`, sortOrder, 'NULLS LAST')
.getMany()
return {

View File

@ -52,9 +52,7 @@ export const updateUserResolver = authorized<
UpdateUserError,
MutationUpdateUserArgs
>(async (_, { input: { name, bio } }, { uid, authTrx }) => {
const user = await userRepository.findOneBy({
id: uid,
})
const user = await userRepository.findById(uid)
if (!user) {
return { errorCodes: [UpdateUserErrorCode.UserNotFound] }
}
@ -92,9 +90,7 @@ export const updateUserProfileResolver = authorized<
UpdateUserProfileError,
MutationUpdateUserProfileArgs
>(async (_, { input: { userId, username, pictureUrl } }, { uid, authTrx }) => {
const user = await userRepository.findOneBy({
id: userId,
})
const user = await userRepository.findById(userId)
if (!user) {
return { errorCodes: [UpdateUserProfileErrorCode.Unauthorized] }
}
@ -117,6 +113,7 @@ export const updateUserProfileResolver = authorized<
profile: {
username: lowerCasedUsername,
},
status: StatusType.Active,
})
if (existingUser?.id) {
return {
@ -161,6 +158,7 @@ export const googleLoginResolver: ResolverFn<
const user = await userRepository.findOneBy({
email,
status: StatusType.Active,
})
if (!user?.id) {
return { errorCodes: [LoginErrorCode.UserNotFound] }
@ -256,9 +254,7 @@ export const getMeUserResolver: ResolverFn<
return undefined
}
const user = await userRepository.findOneBy({
id: claims.uid,
})
const user = await userRepository.findById(claims.uid)
if (!user) {
return undefined
}
@ -282,12 +278,17 @@ export const getUserResolver: ResolverFn<
const userId =
id ||
(username &&
(await userRepository.findOneBy({ profile: { username } }))?.id)
(
await userRepository.findOneBy({
profile: { username },
status: StatusType.Active,
})
)?.id)
if (!userId) {
return { errorCodes: [UserErrorCode.UserNotFound] }
}
const userRecord = await userRepository.findOneBy({ id: userId })
const userRecord = await userRepository.findById(userId)
if (!userRecord) {
return { errorCodes: [UserErrorCode.UserNotFound] }
}
@ -340,9 +341,7 @@ export const updateEmailResolver = authorized<
MutationUpdateEmailArgs
>(async (_, { input: { email } }, { authTrx, uid, log }) => {
try {
const user = await userRepository.findOneBy({
id: uid,
})
const user = await userRepository.findById(uid)
if (!user) {
return {

View File

@ -421,7 +421,7 @@ export function authRouter() {
const { email, password } = req.body
try {
const user = await userRepository.findByEmail(email.trim())
if (!user?.id) {
if (!user || user.status === StatusType.Deleted) {
return res.redirect(
`${env.client.url}/auth/email-login?errorCodes=${LoginErrorCode.UserNotFound}`
)
@ -610,7 +610,7 @@ export function authRouter() {
try {
const user = await userRepository.findByEmail(email)
if (!user) {
if (!user || user.status === StatusType.Deleted) {
return res.redirect(`${env.client.url}/auth/reset-sent`)
}

View File

@ -46,7 +46,7 @@ export async function createMobileEmailSignInResponse(
}
const user = await userRepository.findByEmail(email.trim())
if (!user?.id || !user?.password) {
if (!user || !user.password || user.status === StatusType.Deleted) {
throw new Error('user not found')
}

View File

@ -39,7 +39,7 @@ export function userRouter() {
return
}
try {
const user = await userRepository.findOneBy({ id: claims.uid })
const user = await userRepository.findById(claims.uid)
if (!user) {
res.status(400).send('Bad Request')
return

View File

@ -21,10 +21,7 @@ export const createNewsletterEmail = async (
userId: string,
confirmationCode?: string
): Promise<NewsletterEmail> => {
const user = await userRepository.findOne({
where: { id: userId },
relations: ['profile'],
})
const user = await userRepository.findById(userId)
if (!user) {
return Promise.reject({
errorCode: CreateNewsletterEmailErrorCode.Unauthorized,

View File

@ -42,9 +42,7 @@ export const saveUrlFromEmail = async (
clientRequestId: string,
userId: string
): Promise<boolean> => {
const user = await userRepository.findOneBy({
id: userId,
})
const user = await userRepository.findById(userId)
if (!user) {
return false
}

View File

@ -1,4 +1,4 @@
import { User } from '../entity/user'
import { StatusType, User } from '../entity/user'
import { authTrx } from '../repository'
import { userRepository } from '../repository/user'
@ -21,5 +21,5 @@ export const updateUser = async (userId: string, update: Partial<User>) => {
}
export const findUser = async (id: string): Promise<User | null> => {
return userRepository.findOneBy({ id })
return userRepository.findOneBy({ id, status: StatusType.Active })
}

View File

@ -16,6 +16,7 @@ import { ILike } from 'typeorm'
import { promisify } from 'util'
import { v4 as uuid } from 'uuid'
import { Highlight } from '../entity/highlight'
import { StatusType } from '../entity/user'
import { env } from '../env'
import { PageType, PreparedDocumentInput } from '../generated/graphql'
import { userRepository } from '../repository/user'
@ -470,6 +471,7 @@ export const isProbablyArticle = async (
): Promise<boolean> => {
const user = await userRepository.findOneBy({
email: ILike(email),
status: StatusType.Active,
})
return !!user || subject.includes(ARTICLE_PREFIX)
}

View File

@ -146,7 +146,7 @@ describe('Subscriptions API', () => {
undefined,
SubscriptionType.Newsletter
)
const allSubscriptions = [sub5, ...subscriptions]
const allSubscriptions = [...subscriptions, sub5]
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.subscriptions.subscriptions).to.eql(

View File

@ -0,0 +1,19 @@
-- Type: DO
-- Name: add_unique_to_subscription_url
-- Description: Add unique constraint to the url field on the omnivore.subscription table
BEGIN;
-- Deleting duplicates first to avoid unique constraint violation
WITH DuplicateCTE AS (
SELECT user_id, url,
ROW_NUMBER() OVER (PARTITION BY user_id, url ORDER BY (SELECT NULL)) AS RowNum
FROM omnivore.subscriptions
WHERE type = 'RSS'
)
DELETE FROM omnivore.subscriptions
WHERE (user_id, url) IN (SELECT user_id, url FROM DuplicateCTE WHERE RowNum > 1);
ALTER TABLE omnivore.subscriptions ADD CONSTRAINT subscriptions_user_id_url_key UNIQUE (user_id, url);
COMMIT;

View File

@ -0,0 +1,9 @@
-- Type: UNDO
-- Name: add_unique_to_subscription_url
-- Description: Add unique constraint to the url field on the omnivore.subscription table
BEGIN;
ALTER TABLE omnivore.subscriptions DROP CONSTRAINT IF EXISTS subscriptions_user_id_url_key;
COMMIT;