diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index cb5268ca8..f098332f7 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -1,4 +1,4 @@ -import { DeepPartial } from 'typeorm' +import { DeepPartial, SelectQueryBuilder } from 'typeorm' import { Highlight } from '../entity/highlight' import { Label } from '../entity/label' import { @@ -15,8 +15,11 @@ import { HasFilter, InFilter, LabelFilter, + LabelFilterType, NoFilter, ReadFilter, + SortBy, + SortOrder, SortParams, } from '../utils/search' @@ -102,3 +105,140 @@ export const createLibraryItem = async ( libraryItem.wordCount ?? wordsCount(libraryItem.readableContent ?? ''), }) } + +const buildWhereClause = ( + queryBuilder: SelectQueryBuilder, + userId: string, + args: PageSearchArgs +) => { + if (args.query) { + queryBuilder.andWhere(`tsv @@ websearch_to_tsquery(:query)`, { + query: args.query, + }) + } + + if (args.typeFilter) { + queryBuilder.andWhere(`library_item.item_type = :typeFilter`, { + typeFilter: args.typeFilter, + }) + } + + if (args.inFilter !== InFilter.ALL) { + switch (args.inFilter) { + case InFilter.INBOX: + queryBuilder.andWhere(`library_item.archived_at IS NULL`) + break + case InFilter.ARCHIVE: + queryBuilder.andWhere(`library_item.archived_at IS NOT NULL`) + break + case InFilter.TRASH: + // return only deleted pages within 14 days + queryBuilder.andWhere( + `library_item.state = :state AND deleted_at >= now() - interval '14 days'`, + { + state: LibraryItemState.Deleted, + } + ) + break + } + } + + if (args.readFilter !== ReadFilter.ALL) { + switch (args.readFilter) { + case ReadFilter.READ: + queryBuilder.andWhere('library_item.reading_progress_top_percent >= 98') + break + case ReadFilter.UNREAD: + queryBuilder.andWhere('library_item.reading_progress_top_percent < 98') + break + } + } + + if (args.hasFilters && args.hasFilters.length > 0) { + args.hasFilters.forEach((filter) => { + switch (filter) { + case HasFilter.HIGHLIGHTS: + queryBuilder.andWhere(`library_item.highlights IS NOT NULL`) + break + } + }) + } + + if (args.labelFilters && args.labelFilters.length > 0) { + const includeLabels = args.labelFilters?.filter( + (filter) => filter.type === LabelFilterType.INCLUDE + ) + const excludeLabels = args.labelFilters?.filter( + (filter) => filter.type === LabelFilterType.EXCLUDE + ) + + if (includeLabels && includeLabels.length > 0) { + queryBuilder.andWhere( + `library_item.id IN (SELECT library_item_id FROM library_item_label WHERE name IN (:...includeLabels))`, + { + includeLabels, + } + ) + } + + if (excludeLabels && excludeLabels.length > 0) { + queryBuilder.andWhere( + `library_item.id NOT IN (SELECT library_item_id FROM library_item_label WHERE name IN (:...excludeLabels))`, + { + excludeLabels, + } + ) + } + } + + if (args.dateFilters && args.dateFilters.length > 0) { + args.dateFilters.forEach((filter) => { + queryBuilder.andWhere( + `library_item.${filter.field} between :startDate and :endDate`, + { + startDate: filter.startDate ?? new Date(0), + endDate: filter.endDate ?? new Date(), + } + ) + }) + } + + if (args.termFilters && args.termFilters.length > 0) { + args.termFilters.forEach((filter) => { + queryBuilder.andWhere(`library_item.${filter.field} = :value`, { + value: filter.value, + }) + }) + } +} + +export const searchLibraryItems = async ( + args: PageSearchArgs, + userId: string +): Promise<[LibraryItem[], number] | null> => { + const { from = 0, size = 10, sort } = args + + // default order is descending + const sortOrder = sort?.order || SortOrder.DESCENDING + // default sort by saved_at + const sortField = sort?.by || SortBy.SAVED + + const queryBuilder = entityManager + .createQueryBuilder(LibraryItem, 'library_item') + .select('library_item.*') + .where('library_item.user_id = :userId', { userId }) + + // build the where clause + buildWhereClause(queryBuilder, userId, args) + + // add pagination and sorting + const libraryItems = await queryBuilder + .orderBy(`omnivore.library_item.${sortField}`, sortOrder) + .offset(from) + .limit(size) + .getMany() + + const count = await queryBuilder.getCount() + + return [libraryItems, count] +} diff --git a/packages/api/src/utils/search.ts b/packages/api/src/utils/search.ts index ccd488bb5..6087e1289 100644 --- a/packages/api/src/utils/search.ts +++ b/packages/api/src/utils/search.ts @@ -65,18 +65,18 @@ export interface DateFilter { } export enum SortBy { - SAVED = 'savedAt', - UPDATED = 'updatedAt', + SAVED = 'saved_at', + UPDATED = 'updated_at', SCORE = '_score', - PUBLISHED = 'publishedAt', - READ = 'readAt', - LISTENED = 'listenedAt', - WORDS_COUNT = 'wordsCount', + PUBLISHED = 'published_at', + READ = 'read_at', + LISTENED = 'listened_at', + WORDS_COUNT = 'word_count', } export enum SortOrder { - ASCENDING = 'asc', - DESCENDING = 'desc', + ASCENDING = 'ASC', + DESCENDING = 'DESC', } export interface SortParams {