diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index 3d3fd2765..e2ac8b786 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -518,6 +518,7 @@ export const setBookmarkArticleResolver = authorized< articleID, { state: LibraryItemState.Deleted, + deletedAt: new Date(), }, uid, pubsub @@ -537,12 +538,6 @@ export const setBookmarkArticleResolver = authorized< readableContent: undefined, originalContent: undefined, }), - labels: { - source: 'resolver', - resolver: 'setBookmarkArticleResolver', - userId: uid, - articleID, - }, }) // Make sure article.id instead of userArticle.id has passed. We use it for cache updates return { diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index bc22bf23f..7ce515f27 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -46,6 +46,7 @@ export interface SearchArgs { includeContent?: boolean noFilters?: NoFilter[] siteName?: string + subscription?: string } export interface SearchResultItem { @@ -88,10 +89,15 @@ const buildWhereClause = ( ) => { if (args.query) { queryBuilder - .addSelect('ts_rank_cd(library_item.search_tsv, query)', 'rank') - .addFrom("websearch_to_tsquery('english', ':query')", 'query') - .andWhere('query @@ library_item.search_tsv') + .addSelect( + `ts_rank_cd(library_item.search_tsv, websearch_to_tsquery('english', :query))`, + 'rank' + ) + .andWhere( + `websearch_to_tsquery('english', :query) @@ library_item.search_tsv` + ) .setParameter('query', args.query) + .orderBy('rank', 'DESC') } if (args.typeFilter) { @@ -114,6 +120,16 @@ const buildWhereClause = ( "library_item.state = 'DELETED' AND library_item.deleted_at >= now() - interval '14 days'" ) break + case InFilter.SUBSCRIPTION: + queryBuilder + .andWhere('library_item.subscription_id IS NOT NULL') + .andWhere('library_item.archived_at IS NULL') + break + case InFilter.LIBRARY: + queryBuilder + .andWhere('library_item.subscription_id IS NULL') + .andWhere('library_item.archived_at IS NULL') + break } } @@ -133,11 +149,11 @@ const buildWhereClause = ( switch (filter) { case HasFilter.HIGHLIGHTS: queryBuilder.andWhere( - 'library_item.highlight_annotations IS NOT NULL' + 'array_length(library_item.highlight_annotations, 1) > 0' ) break case HasFilter.LABELS: - queryBuilder.andWhere('library_item.label_names IS NOT NULL') + queryBuilder.andWhere('array_length(library_item.label_names, 1) > 0') break } }) @@ -154,7 +170,7 @@ const buildWhereClause = ( if (includeLabels && includeLabels.length > 0) { includeLabels.forEach((includeLabel) => { queryBuilder.andWhere( - '(lower(library_item.label_names::text)::text[] @> ARRAY[:...includeLabels]::text[] OR lower(library_item.highlight_labels::text)::text[] @> ARRAY[:...includeLabels]::text[])', + 'lower(array_cat(library_item.label_names, library_item.highlight_labels)::text)::text[] @> ARRAY[:...includeLabels]::text[]', { includeLabels: includeLabel.labels, } @@ -164,9 +180,9 @@ const buildWhereClause = ( if (excludeLabels && excludeLabels.length > 0) { queryBuilder.andWhere( - '(NOT lower(library_item.label_names::text)::text[] && ARRAY[:...excludeLabels]::text[] AND NOT lower(library_item.highlight_labels::text)::text[] && ARRAY[:...excludeLabels]::text[])', + 'NOT lower(array_cat(library_item.label_names, library_item.highlight_labels)::text)::text[] && ARRAY[:...excludeLabels]::text[]', { - excludeLabels, + excludeLabels: excludeLabels.flatMap((filter) => filter.labels), } ) } @@ -186,8 +202,8 @@ const buildWhereClause = ( if (args.termFilters && args.termFilters.length > 0) { args.termFilters.forEach((filter) => { - queryBuilder.andWhere(`library_item.${filter.field} = :value`, { - value: filter.value, + queryBuilder.andWhere(`lower(library_item.${filter.field}) = :value`, { + value: filter.value.toLowerCase(), }) }) } @@ -195,9 +211,9 @@ const buildWhereClause = ( if (args.matchFilters && args.matchFilters.length > 0) { args.matchFilters.forEach((filter) => { queryBuilder.andWhere( - `websearch_to_tsquery('english', ':query') @@ library_item.${filter.field}_tsv`, + `websearch_to_tsquery('english', :matchQuery) @@ library_item.${filter.field}_tsv`, { - query: filter.value, + matchQuery: filter.value, } ) }) @@ -224,6 +240,22 @@ const buildWhereClause = ( queryBuilder.andWhere(`library_item.${filter.field} = '{}'`) }) } + + if (args.recommendedBy) { + queryBuilder.innerJoin('library_item.recommendations', 'recommendations') + } + + if (args.subscription) { + queryBuilder + .innerJoin('library_item.subscription', 'subscription') + .andWhere((qb) => { + qb.where('subscription.name = :subscription', { + subscription: args.subscription, + }).orWhere('subscription.url = :subscription', { + subscription: args.subscription, + }) + }) + } } export const searchLibraryItems = async ( @@ -251,7 +283,7 @@ export const searchLibraryItems = async ( buildWhereClause(queryBuilder, args) const libraryItems = await queryBuilder - .orderBy(`library_item.${sortField}`, sortOrder) + .addOrderBy(`library_item.${sortField}`, sortOrder) .offset(from) .limit(size) .getMany() diff --git a/packages/api/src/utils/search.ts b/packages/api/src/utils/search.ts index 4ce603af4..b50419d10 100644 --- a/packages/api/src/utils/search.ts +++ b/packages/api/src/utils/search.ts @@ -42,6 +42,7 @@ export interface SearchFilter { recommendedBy?: string noFilters: NoFilter[] siteName?: string + subscription?: string } export enum LabelFilterType { @@ -94,7 +95,7 @@ export interface NoFilter { field: string } -const parseRecommendedBy = (str?: string): string | undefined => { +const parseStringValue = (str?: string): string | undefined => { if (str === undefined) { return undefined } @@ -268,22 +269,18 @@ const parseFieldFilter = ( return undefined } - let nested = false // normalize the term to lower case const value = str.toLowerCase() switch (field.toUpperCase()) { - case 'RSS': - field = 'rssFeedUrl' - break - case 'NOTE': - field = 'highlights.annotation' - nested = true - break + case 'LANGUAGE': + return { + field: 'item_language', + value, + } } return { - nested, field, value, } @@ -427,9 +424,11 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => { dateFilter && result.dateFilters.push(dateFilter) break } - // term filters case 'subscription': case 'rss': + result.subscription = parseStringValue(keyword.value) + break + // term filters case 'language': { const fieldFilter = parseFieldFilter(keyword.keyword, keyword.value) fieldFilter && result.termFilters.push(fieldFilter) @@ -451,7 +450,7 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => { break } case 'recommendedBy': { - result.recommendedBy = parseRecommendedBy(keyword.value) + result.recommendedBy = parseStringValue(keyword.value) break } case 'no': { diff --git a/packages/db/migrations/0122.do.recommendation.sql b/packages/db/migrations/0122.do.recommendation.sql index e0edb930b..08efba968 100755 --- a/packages/db/migrations/0122.do.recommendation.sql +++ b/packages/db/migrations/0122.do.recommendation.sql @@ -12,6 +12,6 @@ CREATE TABLE omnivore.recommendation ( created_at timestamptz NOT NULL DEFAULT current_timestamp ); -GRANT SELECT, INSERT ON omnivore.library_item TO omnivore_user; +GRANT SELECT, INSERT ON omnivore.recommendation TO omnivore_user; COMMIT;