diff --git a/packages/api/src/entity/user.ts b/packages/api/src/entity/user.ts index a926b5a89..83526f946 100644 --- a/packages/api/src/entity/user.ts +++ b/packages/api/src/entity/user.ts @@ -22,6 +22,7 @@ export enum RegistrationType { export enum StatusType { Active = 'ACTIVE', Pending = 'PENDING', + Deleted = 'DELETED', } @Entity() diff --git a/packages/api/src/repository/user.ts b/packages/api/src/repository/user.ts index 408543c95..a8f327b0d 100644 --- a/packages/api/src/repository/user.ts +++ b/packages/api/src/repository/user.ts @@ -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) { diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index bf26872c5..bf7dc89f9 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -62,11 +62,11 @@ import { } from '../../services/labels' import { createLibraryItem, - findLibraryItemById, findLibraryItemByUrl, findLibraryItemsByPrefix, searchLibraryItems, updateLibraryItem, + updateLibraryItemReadingProgress, updateLibraryItems, } from '../../services/library_item' import { parsedContentToLibraryItem } from '../../services/save_page' @@ -572,14 +572,8 @@ export const saveArticleReadingProgressResolver = authorized< readingProgressTopPercent, }, }, - { uid, pubsub } + { log, pubsub, uid } ) => { - const libraryItem = await findLibraryItemById(id, uid) - - if (!libraryItem) { - return { errorCodes: [SaveArticleReadingProgressErrorCode.NotFound] } - } - if ( readingProgressPercent < 0 || readingProgressPercent > 100 || @@ -590,40 +584,26 @@ export const saveArticleReadingProgressResolver = authorized< ) { return { errorCodes: [SaveArticleReadingProgressErrorCode.BadData] } } - // If we have a top percent, we only save it if it's greater than the current top percent - // or set to zero if the top percent is zero. - const readingProgressTopPercentToSave = readingProgressTopPercent - ? Math.max( - readingProgressTopPercent, - libraryItem.readingProgressTopPercent || 0 - ) - : readingProgressTopPercent === 0 - ? 0 - : undefined - // If setting to zero we accept the update, otherwise we require it - // be greater than the current reading progress. - const updatedPart: QueryDeepPartialEntity = { - readingProgressBottomPercent: - readingProgressPercent === 0 - ? 0 - : Math.max( - readingProgressPercent, - libraryItem.readingProgressBottomPercent - ), - readingProgressHighestReadAnchor: - readingProgressAnchorIndex === 0 - ? 0 - : Math.max( - readingProgressAnchorIndex || 0, - libraryItem.readingProgressHighestReadAnchor - ), - readingProgressTopPercent: readingProgressTopPercentToSave, - readAt: new Date(), - } - const updatedItem = await updateLibraryItem(id, updatedPart, uid, pubsub) + try { + const updatedItem = await updateLibraryItemReadingProgress( + id, + uid, + readingProgressPercent, + readingProgressTopPercent, + readingProgressAnchorIndex, + pubsub + ) + if (!updatedItem) { + return { errorCodes: [SaveArticleReadingProgressErrorCode.BadData] } + } - return { - updatedArticle: libraryItemToArticle(updatedItem), + return { + updatedArticle: libraryItemToArticle(updatedItem), + } + } catch (error) { + log.error('saveArticleReadingProgressResolver error', error) + + return { errorCodes: [SaveArticleReadingProgressErrorCode.Unauthorized] } } } ) diff --git a/packages/api/src/resolvers/importers/uploadImportFileResolver.ts b/packages/api/src/resolvers/importers/uploadImportFileResolver.ts index 4feb518de..f9dccfa37 100644 --- a/packages/api/src/resolvers/importers/uploadImportFileResolver.ts +++ b/packages/api/src/resolvers/importers/uploadImportFileResolver.ts @@ -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], diff --git a/packages/api/src/resolvers/recommendations/index.ts b/packages/api/src/resolvers/recommendations/index.ts index d3062dc5d..31f7da898 100644 --- a/packages/api/src/resolvers/recommendations/index.ts +++ b/packages/api/src/resolvers/recommendations/index.ts @@ -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( async (_, __, { uid, log }) => { try { - const user = await userRepository.findOneBy({ - id: uid, - }) + const user = await userRepository.findById(uid) if (!user) { return { errorCodes: [GroupsErrorCode.Unauthorized], diff --git a/packages/api/src/resolvers/save/index.ts b/packages/api/src/resolvers/save/index.ts index 2acf0ce3b..f5508663d 100644 --- a/packages/api/src/resolvers/save/index.ts +++ b/packages/api/src/resolvers/save/index.ts @@ -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] } } diff --git a/packages/api/src/resolvers/send_install_instructions/index.ts b/packages/api/src/resolvers/send_install_instructions/index.ts index 6435b807b..62d134fbc 100644 --- a/packages/api/src/resolvers/send_install_instructions/index.ts +++ b/packages/api/src/resolvers/send_install_instructions/index.ts @@ -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] } diff --git a/packages/api/src/resolvers/subscriptions/index.ts b/packages/api/src/resolvers/subscriptions/index.ts index db249b9b9..74375441d 100644 --- a/packages/api/src/resolvers/subscriptions/index.ts +++ b/packages/api/src/resolvers/subscriptions/index.ts @@ -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 { @@ -184,7 +185,6 @@ export const subscribeResolver = authorized< url: input.url || undefined, name: input.name || undefined, user: { id: uid }, - status: SubscriptionStatus.Active, type: input.subscriptionType || SubscriptionType.Rss, // default to rss }) ) diff --git a/packages/api/src/resolvers/user/index.ts b/packages/api/src/resolvers/user/index.ts index 8b80e1340..38b0111e2 100644 --- a/packages/api/src/resolvers/user/index.ts +++ b/packages/api/src/resolvers/user/index.ts @@ -1,5 +1,9 @@ import * as jwt from 'jsonwebtoken' -import { RegistrationType, User as UserEntity } from '../../entity/user' +import { + RegistrationType, + StatusType, + User as UserEntity, +} from '../../entity/user' import { env } from '../../env' import { DeleteAccountError, @@ -38,6 +42,7 @@ import { import { userRepository } from '../../repository/user' import { createUser } from '../../services/create_user' import { sendVerificationEmail } from '../../services/send_emails' +import { updateUser } from '../../services/user' import { authorized, userDataToUser } from '../../utils/helpers' import { validateUsername } from '../../utils/usernamePolicy' import { WithDataSourcesContext } from '../types' @@ -47,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] } } @@ -87,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] } } @@ -112,6 +113,7 @@ export const updateUserProfileResolver = authorized< profile: { username: lowerCasedUsername, }, + status: StatusType.Active, }) if (existingUser?.id) { return { @@ -156,6 +158,7 @@ export const googleLoginResolver: ResolverFn< const user = await userRepository.findOneBy({ email, + status: StatusType.Active, }) if (!user?.id) { return { errorCodes: [LoginErrorCode.UserNotFound] } @@ -251,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 } @@ -277,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] } } @@ -313,9 +319,10 @@ export const deleteAccountResolver = authorized< DeleteAccountSuccess, DeleteAccountError, MutationDeleteAccountArgs ->(async (_, { userID }, { authTrx, log }) => { - const result = await authTrx(async (t) => { - return t.withRepository(userRepository).delete(userID) +>(async (_, { userID }, { log }) => { + // soft delete user + const result = await updateUser(userID, { + status: StatusType.Deleted, }) if (!result.affected) { log.error('Error deleting user account') @@ -334,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 { diff --git a/packages/api/src/routers/auth/apple_auth.ts b/packages/api/src/routers/auth/apple_auth.ts index 925e7d37e..99590ff4d 100644 --- a/packages/api/src/routers/auth/apple_auth.ts +++ b/packages/api/src/routers/auth/apple_auth.ts @@ -15,6 +15,7 @@ import { suggestedUsername, } from './jwt_helpers' import { analytics } from '../../utils/analytics' +import { StatusType } from '../../entity/user' const appleBaseURL = 'https://appleid.apple.com' const audienceName = 'app.omnivore.app' @@ -122,6 +123,7 @@ export async function handleAppleWebAuth( const user = await userRepository.findOneBy({ sourceUserId: decodedTokenResult.sourceUserId, source: 'APPLE', + status: StatusType.Active, }) const userId = user?.id diff --git a/packages/api/src/routers/auth/auth_router.ts b/packages/api/src/routers/auth/auth_router.ts index 366c10ada..333391d62 100644 --- a/packages/api/src/routers/auth/auth_router.ts +++ b/packages/api/src/routers/auth/auth_router.ts @@ -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`) } diff --git a/packages/api/src/routers/auth/google_auth.ts b/packages/api/src/routers/auth/google_auth.ts index 781c45dcd..1108b6f23 100644 --- a/packages/api/src/routers/auth/google_auth.ts +++ b/packages/api/src/routers/auth/google_auth.ts @@ -1,6 +1,7 @@ import { google, oauth2_v2 as oauthV2 } from 'googleapis' import { OAuth2Client } from 'googleapis-common' import url from 'url' +import { StatusType } from '../../entity/user' import { env, homePageURL } from '../../env' import { LoginErrorCode } from '../../generated/graphql' import { userRepository } from '../../repository/user' @@ -130,6 +131,7 @@ export async function handleGoogleWebAuth( const user = await userRepository.findOneBy({ email, source: 'GOOGLE', + status: StatusType.Active, }) const userId = user?.id diff --git a/packages/api/src/routers/auth/mobile/sign_in.ts b/packages/api/src/routers/auth/mobile/sign_in.ts index 40d4462bb..4af80f0a6 100644 --- a/packages/api/src/routers/auth/mobile/sign_in.ts +++ b/packages/api/src/routers/auth/mobile/sign_in.ts @@ -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') } diff --git a/packages/api/src/routers/user_router.ts b/packages/api/src/routers/user_router.ts index cc830fc71..8e13ee184 100644 --- a/packages/api/src/routers/user_router.ts +++ b/packages/api/src/routers/user_router.ts @@ -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 diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index 07be9f885..8dab74fc5 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -438,6 +438,75 @@ export const updateLibraryItem = async ( return updatedLibraryItem } +export const updateLibraryItemReadingProgress = async ( + id: string, + userId: string, + bottomPercent: number, + topPercent: number | null = null, + anchorIndex: number | null = null, + pubsub = createPubSubClient() +): Promise => { + // If we have a top percent, we only save it if it's greater than the current top percent + // or set to zero if the top percent is zero. + const result = (await authTrx( + async (tx) => + tx.getRepository(LibraryItem).query( + ` + UPDATE omnivore.library_item + SET reading_progress_top_percent = CASE + WHEN reading_progress_top_percent < $2 THEN $2 + WHEN $2 = 0 THEN 0 + ELSE reading_progress_top_percent + END, + reading_progress_bottom_percent = CASE + WHEN reading_progress_bottom_percent < $3 THEN $3 + WHEN $3 = 0 THEN 0 + ELSE reading_progress_bottom_percent + END, + reading_progress_highest_read_anchor = CASE + WHEN reading_progress_top_percent < $4 THEN $4 + WHEN $4 = 0 THEN 0 + ELSE reading_progress_highest_read_anchor + END, + read_at = now() + WHERE id = $1 AND ( + (reading_progress_top_percent < $2 OR $2 = 0) OR + (reading_progress_bottom_percent < $3 OR $3 = 0) OR + (reading_progress_highest_read_anchor < $4 OR $4 = 0) + ) + RETURNING + id, + reading_progress_top_percent as "readingProgressTopPercent", + reading_progress_bottom_percent as "readingProgressBottomPercent", + reading_progress_highest_read_anchor as "readingProgressHighestReadAnchor", + read_at as "readAt" + `, + [id, topPercent, bottomPercent, anchorIndex] + ), + undefined, + userId + )) as [LibraryItem[], number] + if (result[1] === 0) { + return null + } + + const updatedItem = result[0][0] + await pubsub.entityUpdated>( + EntityType.PAGE, + { + id, + readingProgressBottomPercent: updatedItem.readingProgressBottomPercent, + readingProgressTopPercent: updatedItem.readingProgressTopPercent, + readingProgressHighestReadAnchor: + updatedItem.readingProgressHighestReadAnchor, + readAt: updatedItem.readAt, + }, + userId + ) + + return updatedItem +} + export const createLibraryItems = async ( libraryItems: DeepPartial[], userId: string diff --git a/packages/api/src/services/newsletters.ts b/packages/api/src/services/newsletters.ts index fb9e33cca..f492cf4b5 100644 --- a/packages/api/src/services/newsletters.ts +++ b/packages/api/src/services/newsletters.ts @@ -21,10 +21,7 @@ export const createNewsletterEmail = async ( userId: string, confirmationCode?: string ): Promise => { - const user = await userRepository.findOne({ - where: { id: userId }, - relations: ['profile'], - }) + const user = await userRepository.findById(userId) if (!user) { return Promise.reject({ errorCode: CreateNewsletterEmailErrorCode.Unauthorized, diff --git a/packages/api/src/services/save_url.ts b/packages/api/src/services/save_url.ts index 0f14b1658..3165bb8fb 100644 --- a/packages/api/src/services/save_url.ts +++ b/packages/api/src/services/save_url.ts @@ -42,9 +42,7 @@ export const saveUrlFromEmail = async ( clientRequestId: string, userId: string ): Promise => { - const user = await userRepository.findOneBy({ - id: userId, - }) + const user = await userRepository.findById(userId) if (!user) { return false } diff --git a/packages/api/src/services/user.ts b/packages/api/src/services/user.ts index aed61b8f9..870d9ce6b 100644 --- a/packages/api/src/services/user.ts +++ b/packages/api/src/services/user.ts @@ -1,4 +1,4 @@ -import { User } from '../entity/user' +import { StatusType, User } from '../entity/user' import { authTrx } from '../repository' import { userRepository } from '../repository/user' @@ -13,7 +13,7 @@ export const deleteUser = async (userId: string) => { } export const updateUser = async (userId: string, update: Partial) => { - await authTrx( + return authTrx( async (t) => t.getRepository(User).update(userId, update), undefined, userId @@ -21,5 +21,5 @@ export const updateUser = async (userId: string, update: Partial) => { } export const findUser = async (id: string): Promise => { - return userRepository.findOneBy({ id }) + return userRepository.findOneBy({ id, status: StatusType.Active }) } diff --git a/packages/api/src/utils/parser.ts b/packages/api/src/utils/parser.ts index ae047ef82..eb8ece9c6 100644 --- a/packages/api/src/utils/parser.ts +++ b/packages/api/src/utils/parser.ts @@ -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' @@ -465,6 +466,7 @@ export const isProbablyArticle = async ( ): Promise => { const user = await userRepository.findOneBy({ email: ILike(email), + status: StatusType.Active, }) return !!user || subject.includes(ARTICLE_PREFIX) } diff --git a/packages/api/test/resolvers/article.test.ts b/packages/api/test/resolvers/article.test.ts index e3b13c0d1..083bd389e 100644 --- a/packages/api/test/resolvers/article.test.ts +++ b/packages/api/test/resolvers/article.test.ts @@ -706,7 +706,6 @@ describe('Article API', () => { ).to.eq(75) // Now try to set to a lower value (50), value should not be updated - // refresh index to ensure the reading progress is updated const secondQuery = saveArticleReadingProgressQuery(itemId, 50) const secondRes = await graphqlRequest(secondQuery, authToken).expect(200) expect( diff --git a/packages/api/test/resolvers/subscriptions.test.ts b/packages/api/test/resolvers/subscriptions.test.ts index 07393fbdb..7a9db963d 100644 --- a/packages/api/test/resolvers/subscriptions.test.ts +++ b/packages/api/test/resolvers/subscriptions.test.ts @@ -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( diff --git a/packages/api/test/resolvers/user.test.ts b/packages/api/test/resolvers/user.test.ts index 404f4a09d..82e0ceefc 100644 --- a/packages/api/test/resolvers/user.test.ts +++ b/packages/api/test/resolvers/user.test.ts @@ -9,7 +9,7 @@ import { findProfile } from '../../src/services/profile' import { deleteUser, findUser } from '../../src/services/user' import { hashPassword } from '../../src/utils/auth' import { createTestUser } from '../db' -import { graphqlRequest, request } from '../util' +import { generateFakeUuid, graphqlRequest, request } from '../util' describe('User API', () => { const correctPassword = 'fakePassword' @@ -238,4 +238,58 @@ describe('User API', () => { return graphqlRequest(query, invalidAuthToken).expect(500) }) }) + + describe('Delete account', () => { + const query = (userId: string) => ` + mutation { + deleteAccount( + userID: "${userId}" + ) { + ... on DeleteAccountSuccess { + userID + } + ... on DeleteAccountError { + errorCodes + } + } + } + ` + + let userId: string + let authToken: string + + before(async () => { + const user = await createTestUser('to_delete_user') + const res = await request + .post('/local/debug/fake-user-login') + .send({ fakeEmail: user.email }) + userId = user.id + authToken = res.body.authToken + }) + + after(async () => { + await deleteUser(userId) + }) + + context('when user id is valid', () => { + it('deletes user and responds with 200', async () => { + const response = await graphqlRequest(query(userId), authToken).expect( + 200 + ) + expect(response.body.data.deleteAccount.userID).to.eql(userId) + }) + }) + + context('when user not found', () => { + it('responds with error code UserNotFound', async () => { + const response = await graphqlRequest( + query(generateFakeUuid()), + authToken + ).expect(200) + expect(response.body.data.deleteAccount.errorCodes).to.eql([ + 'USER_NOT_FOUND', + ]) + }) + }) + }) }) diff --git a/packages/db/migrations/0135.do.alter_user_status_type.sql b/packages/db/migrations/0135.do.alter_user_status_type.sql new file mode 100755 index 000000000..da4f39eef --- /dev/null +++ b/packages/db/migrations/0135.do.alter_user_status_type.sql @@ -0,0 +1,9 @@ +-- Type: DO +-- Name: alter_user_status_type +-- Description: Add DELETED to the user_status_type enum + +BEGIN; + +ALTER TYPE user_status_type ADD VALUE 'DELETED'; + +COMMIT; diff --git a/packages/db/migrations/0135.undo.alter_user_status_type.sql b/packages/db/migrations/0135.undo.alter_user_status_type.sql new file mode 100755 index 000000000..a39f1f7ba --- /dev/null +++ b/packages/db/migrations/0135.undo.alter_user_status_type.sql @@ -0,0 +1,9 @@ +-- Type: UNDO +-- Name: alter_user_status_type +-- Description: Add DELETED to the user_status_type enum + +BEGIN; + +ALTER TYPE user_status_type DROP VALUE IF EXISTS 'DELETED'; + +COMMIT; diff --git a/packages/db/migrations/0136.do.add_unique_to_subscription_url.sql b/packages/db/migrations/0136.do.add_unique_to_subscription_url.sql new file mode 100755 index 000000000..2bfea4029 --- /dev/null +++ b/packages/db/migrations/0136.do.add_unique_to_subscription_url.sql @@ -0,0 +1,18 @@ +-- 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 id, ROW_NUMBER() OVER (PARTITION BY user_id, url ORDER BY status, last_fetched_at DESC NULLS LAST) AS row_number + FROM omnivore.subscriptions + WHERE type = 'RSS' +) +DELETE FROM omnivore.subscriptions + WHERE id IN (SELECT id FROM DuplicateCTE WHERE row_number > 1); + +ALTER TABLE omnivore.subscriptions ADD CONSTRAINT subscriptions_user_id_url_key UNIQUE (user_id, url); + +COMMIT; diff --git a/packages/db/migrations/0136.undo.add_unique_to_subscription_url.sql b/packages/db/migrations/0136.undo.add_unique_to_subscription_url.sql new file mode 100755 index 000000000..72071eab2 --- /dev/null +++ b/packages/db/migrations/0136.undo.add_unique_to_subscription_url.sql @@ -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; diff --git a/packages/db/migrations/0137.do.alter_library_item_tsv_update_trigger.sql b/packages/db/migrations/0137.do.alter_library_item_tsv_update_trigger.sql new file mode 100755 index 000000000..aa7d266d8 --- /dev/null +++ b/packages/db/migrations/0137.do.alter_library_item_tsv_update_trigger.sql @@ -0,0 +1,15 @@ +-- Type: DO +-- Name: alter_library_item_tsv_update_trigger +-- Description: Alter library_item_tsv_update trigger on omnivore.library_item table to add conditions + +BEGIN; + +DROP TRIGGER IF EXISTS library_item_tsv_update ON omnivore.library_item; + +CREATE TRIGGER library_item_tsv_update + BEFORE INSERT OR UPDATE OF readable_content, site_name, title, author, description, note, highlight_annotations + ON omnivore.library_item + FOR EACH ROW + EXECUTE PROCEDURE update_library_item_tsv(); + +COMMIT; diff --git a/packages/db/migrations/0137.undo.alter_library_item_tsv_update_trigger.sql b/packages/db/migrations/0137.undo.alter_library_item_tsv_update_trigger.sql new file mode 100755 index 000000000..cb367e944 --- /dev/null +++ b/packages/db/migrations/0137.undo.alter_library_item_tsv_update_trigger.sql @@ -0,0 +1,15 @@ +-- Type: UNDO +-- Name: alter_library_item_tsv_update_trigger +-- Description: Alter library_item_tsv_update trigger on omnivore.library_item table to add conditions + +BEGIN; + +DROP TRIGGER IF EXISTS library_item_tsv_update ON omnivore.library_item; + +CREATE TRIGGER library_item_tsv_update + BEFORE INSERT OR UPDATE + ON omnivore.library_item + FOR EACH ROW + EXECUTE PROCEDURE update_library_item_tsv(); + +COMMIT; diff --git a/packages/db/package.json b/packages/db/package.json index bf7cfd766..fce36c4af 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -9,7 +9,6 @@ "author": "", "license": "ISC", "dependencies": { - "@elastic/elasticsearch": "~7.12.0", "dotenv": "^8.2.0", "pg": "^8.3.0", "postgrator": "^4.1.1", diff --git a/packages/rule-handler/src/filter.ts b/packages/rule-handler/src/filter.ts index a3c43e902..2eb365c05 100644 --- a/packages/rule-handler/src/filter.ts +++ b/packages/rule-handler/src/filter.ts @@ -6,6 +6,9 @@ interface SearchResponse { edges: Edge[] } } + errors?: { + message: string + }[] } interface Edge { @@ -28,7 +31,6 @@ interface Label { } export const search = async ( - userId: string, apiEndpoint: string, auth: string, query: string @@ -75,6 +77,12 @@ export const search = async ( } ) + if (response.data.errors) { + console.error(response.data.errors) + + return [] + } + const edges = response.data.data.search.edges if (edges.length === 0) { return [] @@ -89,14 +97,13 @@ export const search = async ( } export const filterPage = async ( - userId: string, apiEndpoint: string, auth: string, filter: string, pageId: string ): Promise => { filter += ` includes:${pageId}` - const pages = await search(userId, apiEndpoint, auth, filter) + const pages = await search(apiEndpoint, auth, filter) return pages.length > 0 ? pages[0] : null } diff --git a/packages/rule-handler/src/rule.ts b/packages/rule-handler/src/rule.ts index b7345fb2c..ef80aa973 100644 --- a/packages/rule-handler/src/rule.ts +++ b/packages/rule-handler/src/rule.ts @@ -93,7 +93,6 @@ export const triggerActions = async ( } const filteredPage = await filterPage( - userId, apiEndpoint, authToken, rule.filter, diff --git a/yarn.lock b/yarn.lock index 3125e7f03..90f5c0f62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2228,17 +2228,6 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@elastic/elasticsearch@~7.12.0": - version "7.12.0" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.12.0.tgz#dbb51a2841f644b670a56d8c15899e860928856f" - integrity sha512-GquUEytCijFRPEk3DKkkDdyhspB3qbucVQOwih9uNyz3iz804I+nGBUsFo2LwVvLQmQfEM0IY2+yoYfEz5wMug== - dependencies: - debug "^4.3.1" - hpagent "^0.1.1" - ms "^2.1.3" - pump "^3.0.0" - secure-json-parse "^2.3.1" - "@emotion/cache@^10.0.27": version "10.0.29" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" @@ -15835,11 +15824,6 @@ hpack.js@^2.1.6: readable-stream "^2.0.1" wbuf "^1.1.0" -hpagent@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.2.tgz#cab39c66d4df2d4377dbd212295d878deb9bdaa9" - integrity sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ== - html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" @@ -20177,7 +20161,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: +ms@2.1.3, ms@^2.0.0, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -24468,11 +24452,6 @@ search-query-parser@^1.6.0: resolved "https://registry.yarnpkg.com/search-query-parser/-/search-query-parser-1.6.0.tgz#d69ade33f3685cae25613a70189b7b18970b46f1" integrity sha512-bhf+phLlKF38nuniwLcVHWPArHGdzenlPhPi955CR3vm1QQifXIuPHwAffhjapojdVVzmv4hgIJ6NOX1d/w+Uw== -secure-json-parse@^2.3.1: - version "2.4.0" - resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.4.0.tgz#5aaeaaef85c7a417f76271a4f5b0cc3315ddca85" - integrity sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg== - selderee@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/selderee/-/selderee-0.6.0.tgz#f3bee66cfebcb6f33df98e4a1df77388b42a96f7"