diff --git a/packages/api/src/events/user/profile_created.ts b/packages/api/src/events/user/profile_created.ts deleted file mode 100644 index 0af9326e0..000000000 --- a/packages/api/src/events/user/profile_created.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - EntitySubscriberInterface, - EventSubscriber, - InsertEvent, -} from 'typeorm' -import { Profile } from '../../entity/profile' -import { createDefaultFiltersForUser } from '../../services/create_user' -import { addPopularReadsForNewUser } from '../../services/popular_reads' - -@EventSubscriber() -export class AddPopularReadsToNewUser - implements EntitySubscriberInterface -{ - listenTo() { - return Profile - } - - async afterInsert(event: InsertEvent): Promise { - await addPopularReadsForNewUser(event.entity.user.id, event.manager) - } -} - -@EventSubscriber() -export class AddDefaultFiltersToNewUser - implements EntitySubscriberInterface -{ - listenTo() { - return Profile - } - - async afterInsert(event: InsertEvent): Promise { - await createDefaultFiltersForUser(event.manager)(event.entity.user.id) - } -} diff --git a/packages/api/src/services/create_user.ts b/packages/api/src/services/create_user.ts index 4648658c8..6ded934b4 100644 --- a/packages/api/src/services/create_user.ts +++ b/packages/api/src/services/create_user.ts @@ -15,6 +15,7 @@ import { analytics } from '../utils/analytics' import { IntercomClient } from '../utils/intercom' import { logger } from '../utils/logger' import { validateUsername } from '../utils/usernamePolicy' +import { addPopularReadsForNewUser } from './popular_reads' import { sendConfirmationEmail } from './send_emails' export const MAX_RECORDS_LIMIT = 1000 @@ -103,6 +104,9 @@ export const createUser = async (input: { }) } + await addPopularReadsForNewUser(user.id, t) + await createDefaultFiltersForUser(t)(user.id) + return [user, profile] } ) @@ -146,7 +150,7 @@ export const createUser = async (input: { return [user, profile] } -export const createDefaultFiltersForUser = +const createDefaultFiltersForUser = (t: EntityManager) => async (userId: string): Promise => { const defaultFilters = [ diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index 1d869fbfc..f9f8d5cb7 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -1,4 +1,13 @@ -import { DeepPartial, SelectQueryBuilder } from 'typeorm' +import { + Between, + DeepPartial, + In, + IsNull, + LessThan, + MoreThan, + Not, + SelectQueryBuilder, +} from 'typeorm' import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity' import { EntityLabel } from '../entity/entity_label' import { Highlight } from '../entity/highlight' @@ -106,10 +115,14 @@ const buildWhereClause = ( if (args.inFilter !== InFilter.ALL) { switch (args.inFilter) { case InFilter.INBOX: - queryBuilder.andWhere('library_item.archived_at IS NULL') + queryBuilder.andWhere({ + archivedAt: IsNull(), + }) break case InFilter.ARCHIVE: - queryBuilder.andWhere('library_item.archived_at IS NOT NULL') + queryBuilder.andWhere({ + archivedAt: Not(IsNull()), + }) break case InFilter.TRASH: // return only deleted pages within 14 days @@ -119,16 +132,20 @@ const buildWhereClause = ( break case InFilter.SUBSCRIPTION: queryBuilder - .andWhere('library_item.subscription IS NOT NULL') .andWhere("NOT ('library' ILIKE ANY (library_item.label_names))") - .andWhere('library_item.archived_at IS NULL') + .andWhere({ + subscription: Not(IsNull()), + archivedAt: IsNull(), + }) break case InFilter.LIBRARY: queryBuilder .andWhere( "(library_item.subscription IS NULL OR 'library' ILIKE ANY (library_item.label_names))" ) - .andWhere('library_item.archived_at IS NULL') + .andWhere({ + archivedAt: IsNull(), + }) break } } @@ -136,10 +153,17 @@ const buildWhereClause = ( if (args.readFilter !== ReadFilter.ALL) { switch (args.readFilter) { case ReadFilter.READ: - queryBuilder.andWhere('library_item.reading_progress_top_percent >= 98') + queryBuilder.andWhere({ + readingProgressBottomPercent: MoreThan(98), + }) + break + case ReadFilter.READING: + queryBuilder.andWhere({ readingProgressBottomPercent: Between(2, 98) }) break case ReadFilter.UNREAD: - queryBuilder.andWhere('library_item.reading_progress_top_percent < 98') + queryBuilder.andWhere({ + readingProgressBottomPercent: LessThan(2), + }) break } } @@ -148,12 +172,10 @@ const buildWhereClause = ( args.hasFilters.forEach((filter) => { switch (filter) { case HasFilter.HIGHLIGHTS: - queryBuilder.andWhere( - 'array_length(library_item.highlight_annotations, 1) > 0' - ) + queryBuilder.andWhere("library_item.highlight_annotations <> '{}'") break case HasFilter.LABELS: - queryBuilder.andWhere('array_length(library_item.label_names, 1) > 0') + queryBuilder.andWhere("library_item.label_names <> '{}'") break } }) @@ -225,18 +247,20 @@ const buildWhereClause = ( } if (args.ids && args.ids.length > 0) { - queryBuilder.andWhere('library_item.id IN (:...ids)', { ids: args.ids }) + queryBuilder.andWhere({ + id: In(args.ids), + }) } if (!args.includePending) { - queryBuilder.andWhere('library_item.state != :state', { - state: LibraryItemState.Processing, + queryBuilder.andWhere({ + state: Not(LibraryItemState.Processing), }) } if (!args.includeDeleted && args.inFilter !== InFilter.TRASH) { - queryBuilder.andWhere('library_item.state != :state', { - state: LibraryItemState.Deleted, + queryBuilder.andWhere({ + state: Not(LibraryItemState.Deleted), }) } @@ -303,7 +327,7 @@ export const searchLibraryItems = async ( const queryBuilder = tx .createQueryBuilder(LibraryItem, 'library_item') .select(selectColumns) - .where('library_item.user_id = :userId', { userId }) + .where({ user: { id: userId } }) // build the where clause buildWhereClause(queryBuilder, args) diff --git a/packages/api/src/utils/search.ts b/packages/api/src/utils/search.ts index f5d9e9b6c..c3af64881 100644 --- a/packages/api/src/utils/search.ts +++ b/packages/api/src/utils/search.ts @@ -14,6 +14,7 @@ import { InputMaybe, PageType, SortParams } from '../generated/graphql' export enum ReadFilter { ALL, READ, + READING, UNREAD, } @@ -110,6 +111,8 @@ const parseIsFilter = (str: string | undefined): ReadFilter => { switch (str?.toUpperCase()) { case 'READ': return ReadFilter.READ + case 'READING': + return ReadFilter.READING case 'UNREAD': return ReadFilter.UNREAD } diff --git a/packages/api/test/resolvers/article.test.ts b/packages/api/test/resolvers/article.test.ts index 2959a2072..85b788f90 100644 --- a/packages/api/test/resolvers/article.test.ts +++ b/packages/api/test/resolvers/article.test.ts @@ -818,6 +818,7 @@ describe('Article API', () => { let keyword = '' before(async () => { + const readingProgressArray = [0, 2, 97, 98, 100] // Create some test items for (let i = 0; i < 5; i++) { const itemToSave: DeepPartial = { @@ -827,6 +828,7 @@ describe('Article API', () => { slug: 'test slug', originalUrl: `${url}/${i}`, siteName: 'Example', + readingProgressBottomPercent: readingProgressArray[i], } const item = await createLibraryItem(itemToSave, user.id) items.push(item) @@ -887,15 +889,39 @@ describe('Article API', () => { keyword = `'${searchedKeyword}' is:unread` }) - it('should return unread articles in descending order', async () => { + it('returns unread articles in descending order', async () => { const res = await graphqlRequest(query, authToken).expect(200) - expect(res.body.data.search.edges.length).to.eq(5) + expect(res.body.data.search.edges.length).to.eq(1) + expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id) + }) + }) + + context('when is:reading is in the query', () => { + before(() => { + keyword = `'${searchedKeyword}' is:reading` + }) + + it('returns reading articles in descending order', async () => { + const res = await graphqlRequest(query, authToken).expect(200) + + expect(res.body.data.search.edges.length).to.eq(3) + expect(res.body.data.search.edges[0].node.id).to.eq(items[3].id) + expect(res.body.data.search.edges[1].node.id).to.eq(items[2].id) + expect(res.body.data.search.edges[2].node.id).to.eq(items[1].id) + }) + }) + + context('when is:read is in the query', () => { + before(() => { + keyword = `'${searchedKeyword}' is:read` + }) + + it('returns fully read articles in descending order', async () => { + const res = await graphqlRequest(query, authToken).expect(200) + + expect(res.body.data.search.edges.length).to.eq(1) expect(res.body.data.search.edges[0].node.id).to.eq(items[4].id) - expect(res.body.data.search.edges[1].node.id).to.eq(items[3].id) - expect(res.body.data.search.edges[2].node.id).to.eq(items[2].id) - expect(res.body.data.search.edges[3].node.id).to.eq(items[1].id) - expect(res.body.data.search.edges[4].node.id).to.eq(items[0].id) }) }) @@ -1122,7 +1148,7 @@ describe('Article API', () => { slug: 'test slug 2', originalUrl: `${url}/test2`, archivedAt: new Date(), - readingProgressTopPercent: 100, + readingProgressBottomPercent: 100, }, { user, @@ -1140,7 +1166,7 @@ describe('Article API', () => { await deleteLibraryItems(items, user.id) }) - it('returns unfinished archived items', async () => { + it('returns unread archived items', async () => { const res = await graphqlRequest(query, authToken).expect(200) expect(res.body.data.search.pageInfo.totalCount).to.eq(1) @@ -1221,7 +1247,7 @@ describe('Article API', () => { slug: 'test slug 2', originalUrl: `${url}/test2`, deletedAt: new Date(), - readingProgressTopPercent: 100, + readingProgressBottomPercent: 100, }, { user,