remove duplicate rss subscriptions by user and url; create a unique constraint
This commit is contained in:
@ -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) {
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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] }
|
||||
}
|
||||
|
||||
@ -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] }
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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`)
|
||||
}
|
||||
|
||||
|
||||
@ -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')
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 })
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
19
packages/db/migrations/0136.do.add_unique_to_subscription_url.sql
Executable file
19
packages/db/migrations/0136.do.add_unique_to_subscription_url.sql
Executable 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;
|
||||
9
packages/db/migrations/0136.undo.add_unique_to_subscription_url.sql
Executable file
9
packages/db/migrations/0136.undo.add_unique_to_subscription_url.sql
Executable 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;
|
||||
Reference in New Issue
Block a user