From d42f257f9ec4cd518b189b6217d2cafffe5836c7 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 6 Dec 2023 21:09:30 +0800 Subject: [PATCH 1/4] fix searching for multiple labels in iOS --- packages/api/src/services/library_item.ts | 8 ++++---- packages/api/src/utils/search.ts | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index 4264fbc1a..3baad9cc3 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -181,15 +181,14 @@ export const buildQuery = ( return null } - const param = 'implicit_field' - const alias = 'rank' + const param = `implicit_field_${parameters.length}` + const alias = `rank_${parameters.length}` selects.push({ column: `ts_rank_cd(library_item.search_tsv, websearch_to_tsquery('english', :${param}))`, alias, }) - // always sort by rank first - orders.unshift({ by: alias, order: SortOrder.DESCENDING }) + orders.push({ by: alias, order: SortOrder.DESCENDING }) return escapeQueryWithParameters( `websearch_to_tsquery('english', :${param}) @@ library_item.search_tsv`, @@ -532,6 +531,7 @@ export const buildQuery = ( } case 'use': case 'mode': + case 'event': // mode is ignored and used only by the frontend return null case 'readPosition': diff --git a/packages/api/src/utils/search.ts b/packages/api/src/utils/search.ts index 167b80acf..196cd652f 100644 --- a/packages/api/src/utils/search.ts +++ b/packages/api/src/utils/search.ts @@ -7,6 +7,8 @@ export const parseSearchQuery = (query: string): LiqeQuery => { .replace('in:library', 'no:subscription') // compatibility with old search // wrap the value behind colon in quotes if it's not already .replace(/(\w+):("([^"]+)"|([^")\s]+))/g, '$1:"$3$4"') + // remove any quotes that are in the array value for example: label:"test","test2" -> label:"test,test2" + .replace(/","/g, ',') return parse(searchQuery) } From ed29721728921386a65c13a69c36f7e24fa48e9a Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 6 Dec 2023 22:06:47 +0800 Subject: [PATCH 2/4] fix bulk action query limited to 1000 char --- packages/api/src/resolvers/article/index.ts | 19 ++++++++++-- packages/api/src/services/library_item.ts | 34 ++++++++++++--------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index a1e7c33cb..162dc380f 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -822,8 +822,12 @@ export const bulkActionResolver = authorized< }, }) - // the query size is limited to 255 characters - if (!query || query.length > 255) { + // the query size is limited to 1000 characters + if (!query || query.length > 1000) { + log.error('bulkActionResolver error', { + error: 'QueryTooLong', + query, + }) return { errorCodes: [BulkActionErrorCode.BadRequest] } } @@ -837,7 +841,16 @@ export const bulkActionResolver = authorized< labels = await findLabelsByIds(labelIds, uid) } - await updateLibraryItems(action, query, uid, labels, args) + await updateLibraryItems( + action, + { + query, + useFolders: query.includes('use:folders'), + }, + uid, + labels, + args + ) return { success: true } } catch (error) { diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index 3baad9cc3..eaef581b4 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -80,11 +80,11 @@ export interface SearchResultItem { } export enum SortBy { - SAVED = 'saved_at', - UPDATED = 'updated_at', - PUBLISHED = 'published_at', - READ = 'read_at', - WORDS_COUNT = 'word_count', + SAVED = 'saved', + UPDATED = 'updated', + PUBLISHED = 'published', + READ = 'read', + WORDS_COUNT = 'wordsCount', } export enum SortOrder { @@ -102,6 +102,10 @@ interface Select { alias?: string } +const paramtersToObject = (parameters: ObjectLiteral[]) => { + return parameters.reduce((a, b) => ({ ...a, ...b }), {}) +} + export const sortParamsToSort = ( sortParams: InputMaybe | undefined ) => { @@ -688,7 +692,7 @@ export const searchLibraryItems = async ( // add where clause from query queryBuilder .andWhere(query) - .setParameters(parameters.reduce((a, b) => ({ ...a, ...b }), {})) + .setParameters(paramtersToObject(parameters)) } const count = await queryBuilder.getCount() @@ -1011,7 +1015,7 @@ export const countByCreatedAt = async ( export const updateLibraryItems = async ( action: BulkActionType, - query: string, + searchArgs: SearchArgs, userId: string, labels?: Label[], args?: unknown @@ -1065,19 +1069,21 @@ export const updateLibraryItems = async ( throw new Error('Invalid bulk action') } - const searchQuery = parseSearchQuery(query) + if (!searchArgs.query) { + throw new Error('Search query is required') + } + + const searchQuery = parseSearchQuery(searchArgs.query) + const parameters: ObjectLiteral[] = [] + const query = buildQuery(searchQuery, parameters) await authTrx(async (tx) => { const queryBuilder = tx .createQueryBuilder(LibraryItem, 'library_item') .where('library_item.user_id = :userId', { userId }) - const parameters: ObjectLiteral[] = [] - const whereClause = buildQuery(searchQuery, parameters) - if (whereClause) { - queryBuilder - .andWhere(whereClause) - .setParameters(parameters.reduce((a, b) => ({ ...a, ...b }), {})) + if (query) { + queryBuilder.andWhere(query).setParameters(paramtersToObject(parameters)) } if (addLabels) { From 618a5dc6a884b85727cb321ceec23cc051a9674e Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 6 Dec 2023 22:23:32 +0800 Subject: [PATCH 3/4] add test cases --- packages/api/test/resolvers/article.test.ts | 55 +++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/api/test/resolvers/article.test.ts b/packages/api/test/resolvers/article.test.ts index 17b66eff2..e46e24c5d 100644 --- a/packages/api/test/resolvers/article.test.ts +++ b/packages/api/test/resolvers/article.test.ts @@ -1702,6 +1702,61 @@ describe('Article API', () => { expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id) }) }) + + context('when label:test1,test2 is in the query', () => { + let items: LibraryItem[] = [] + let label1: Label + let label2: Label + + before(async () => { + keyword = 'label:test1,test2' + // Create some test items + label1 = await createLabel('test1', '', user.id) + label2 = await createLabel('test2', '', user.id) + items = await createLibraryItems( + [ + { + user, + title: 'test title 1', + readableContent: '

test 1

', + slug: 'test slug 1', + originalUrl: `${url}/test1`, + }, + { + user, + title: 'test title 2', + readableContent: '

test 2

', + slug: 'test slug 2', + originalUrl: `${url}/test2`, + }, + { + user, + title: 'test title 3', + readableContent: '

test 3

', + slug: 'test slug 3', + originalUrl: `${url}/test3`, + }, + ], + user.id + ) + await saveLabelsInLibraryItem([label1], items[0].id, user.id) + await saveLabelsInLibraryItem([label2], items[1].id, user.id) + }) + + after(async () => { + await deleteLabels({ id: label1.id }, user.id) + await deleteLabels({ id: label2.id }, user.id) + await deleteLibraryItems(items, user.id) + }) + + it('returns items with label test1 or test2', async () => { + const res = await graphqlRequest(query, authToken).expect(200) + + expect(res.body.data.search.pageInfo.totalCount).to.eq(2) + expect(res.body.data.search.edges[0].node.id).to.eq(items[1].id) + expect(res.body.data.search.edges[1].node.id).to.eq(items[0].id) + }) + }) }) describe('TypeaheadSearch API', () => { From 5f09165128c1f49edf9ab1d3ca35256eca189298 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 7 Dec 2023 10:55:25 +0800 Subject: [PATCH 4/4] allow bulk action on max 100 items --- packages/api/src/resolvers/article/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index 162dc380f..49e025502 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -822,8 +822,8 @@ export const bulkActionResolver = authorized< }, }) - // the query size is limited to 1000 characters - if (!query || query.length > 1000) { + // the query size is limited to 4000 characters to allow for 100 items + if (!query || query.length > 4000) { log.error('bulkActionResolver error', { error: 'QueryTooLong', query,