diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index f2ae8f800..aa57c1543 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -649,6 +649,7 @@ export const searchResolver = authorized< size: first + 1, // fetch one more item to get next cursor sort: searchQuery.sort, includePending: true, + includeContent: params.includeContent || false, ...searchQuery, }, uid diff --git a/packages/api/src/services/integrations/readwise.ts b/packages/api/src/services/integrations/readwise.ts index 28c444cfc..f111599a8 100644 --- a/packages/api/src/services/integrations/readwise.ts +++ b/packages/api/src/services/integrations/readwise.ts @@ -6,7 +6,7 @@ import { LibraryItem } from '../../entity/library_item' import { env } from '../../env' import { wait } from '../../utils/helpers' import { logger } from '../../utils/logger' -import { getHighlightUrl } from '../highlights' +import { findHighlightsByLibraryItemId, getHighlightUrl } from '../highlights' import { IntegrationService } from './integration' interface ReadwiseHighlight { @@ -64,10 +64,12 @@ export class ReadwiseIntegration extends IntegrationService { ): Promise => { let result = true - const highlights = items.flatMap(this.libraryItemToReadwiseHighlight) + const highlights = await Promise.all( + items.map((item) => this.libraryItemToReadwiseHighlight(item)) + ) // If there are no highlights, we will skip the sync if (highlights.length > 0) { - result = await this.syncWithReadwise(integration.token, highlights) + result = await this.syncWithReadwise(integration.token, highlights.flat()) } // update integration syncedAt if successful @@ -84,10 +86,16 @@ export class ReadwiseIntegration extends IntegrationService { return result } - libraryItemToReadwiseHighlight = (item: LibraryItem): ReadwiseHighlight[] => { - if (!item.highlights) return [] + libraryItemToReadwiseHighlight = async ( + item: LibraryItem + ): Promise => { + let highlights = item.highlights + if (!highlights) { + highlights = await findHighlightsByLibraryItemId(item.id, item.user.id) + } + const category = item.siteName === 'Twitter' ? 'tweets' : 'articles' - return item.highlights + return highlights .map((highlight) => { // filter out highlights that are not of type highlight or have no quote if ( diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index 1dcdb146d..1d869fbfc 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -6,7 +6,7 @@ import { Label } from '../entity/label' import { LibraryItem, LibraryItemState } from '../entity/library_item' import { BulkActionType } from '../generated/graphql' import { createPubSubClient, EntityType } from '../pubsub' -import { authTrx } from '../repository' +import { authTrx, getColumns } from '../repository' import { libraryItemRepository } from '../repository/library_item' import { wordsCount } from '../utils/helpers' import { @@ -17,6 +17,7 @@ import { LabelFilter, LabelFilterType, NoFilter, + RangeFilter, ReadFilter, Sort, SortBy, @@ -42,6 +43,7 @@ export interface SearchArgs { recommendedBy?: string includeContent?: boolean noFilters?: NoFilter[] + rangeFilters?: RangeFilter[] } export interface SearchResultItem { @@ -258,6 +260,22 @@ const buildWhereClause = ( ) } } + + if (args.includeContent) { + queryBuilder.addSelect('library_item.readableContent') + } + + if (args.rangeFilters && args.rangeFilters.length > 0) { + args.rangeFilters.forEach((filter, i) => { + const param = `range_${filter.field}_${i}` + queryBuilder.andWhere( + `library_item.${filter.field} ${filter.operator} ${param}`, + { + [param]: filter.value, + } + ) + }) + } } export const searchLibraryItems = async ( @@ -271,11 +289,20 @@ export const searchLibraryItems = async ( // default sort by saved_at const sortField = sort?.by || SortBy.SAVED + const selectColumns = getColumns(libraryItemRepository) + .map((column) => `library_item.${column}`) + .filter( + (column) => + column !== 'library_item.readableContent' && + column !== 'library_item.originalContent' + ) + // add pagination and sorting return authTrx( async (tx) => { const queryBuilder = tx .createQueryBuilder(LibraryItem, 'library_item') + .select(selectColumns) .where('library_item.user_id = :userId', { userId }) // build the where clause diff --git a/packages/api/src/utils/search.ts b/packages/api/src/utils/search.ts index 5bbe09cde..f5d9e9b6c 100644 --- a/packages/api/src/utils/search.ts +++ b/packages/api/src/utils/search.ts @@ -40,6 +40,7 @@ export interface SearchFilter { ids: string[] recommendedBy?: string noFilters: NoFilter[] + rangeFilters: RangeFilter[] } export enum LabelFilterType { @@ -63,6 +64,12 @@ export interface DateFilter { endDate?: Date } +export interface RangeFilter { + field: string + operator: string + value: number +} + export enum SortBy { SAVED = 'savedAt', UPDATED = 'updatedAt', @@ -255,6 +262,40 @@ const parseDateFilter = ( } } +const parseRangeFilter = ( + field: string, + str?: string +): RangeFilter | undefined => { + if (str === undefined) { + return undefined + } + + switch (field.toUpperCase()) { + case 'WORDSCOUNT': + field = 'wordCount' + break + default: + return undefined + } + + const operatorRegex = /([<>]=?)/ + const operator = str.match(operatorRegex)?.[0] + if (!operator) { + return undefined + } + + const value = str.replace(operatorRegex, '') + if (!value) { + return undefined + } + + return { + field, + operator, + value: Number(value), + } +} + const parseFieldFilter = ( field: string, str?: string @@ -323,6 +364,7 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => { matchFilters: [], ids: [], noFilters: [], + rangeFilters: [], } if (!searchQuery) { @@ -337,6 +379,7 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => { matchFilters: [], ids: [], noFilters: [], + rangeFilters: [], } } @@ -364,6 +407,7 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => { 'site', 'note', 'rss', + 'wordCount', ], tokenize: true, }) @@ -460,6 +504,11 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => { case 'mode': // mode is ignored and used only by the frontend break + case 'wordCount': { + const rangeFilter = parseRangeFilter(keyword.keyword, keyword.value) + rangeFilter && result.rangeFilters.push(rangeFilter) + break + } } } }