diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index 3511020ab..515abdb6b 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -171,22 +171,61 @@ const buildWhereClause = ( if (includeLabels && includeLabels.length > 0) { includeLabels.forEach((includeLabel, i) => { const param = `includeLabels_${i}` - queryBuilder.andWhere( - `lower(array_cat(library_item.label_names, library_item.highlight_labels)::text)::text[] && ARRAY[:...${param}]::text[]`, - { - [param]: includeLabel.labels, - } + const hasWildcard = includeLabel.labels.some((label) => + label.includes('*') ) + if (hasWildcard) { + queryBuilder.andWhere( + new Brackets((qb) => { + includeLabel.labels.forEach((label, j) => { + const param = `includeLabels_${i}_${j}` + qb.orWhere( + `array_to_string(array_cat(library_item.label_names, library_item.highlight_labels)::text[], ',') ILIKE :${param}`, + { + [param]: label.replace(/\*/g, '%'), + } + ) + }) + }) + ) + } else { + queryBuilder.andWhere( + `lower(array_cat(library_item.label_names, library_item.highlight_labels)::text)::text[] && ARRAY[:...${param}]::text[]`, + { + [param]: includeLabel.labels, + } + ) + } }) } if (excludeLabels && excludeLabels.length > 0) { - queryBuilder.andWhere( - 'NOT lower(array_cat(library_item.label_names, library_item.highlight_labels)::text)::text[] && ARRAY[:...excludeLabels]::text[]', - { - excludeLabels: excludeLabels.flatMap((filter) => filter.labels), - } - ) + const labels = excludeLabels.flatMap((filter) => filter.labels) + + const hasWildcard = labels.some((label) => label.includes('*')) + + if (hasWildcard) { + queryBuilder.andWhere( + new Brackets((qb) => { + labels.forEach((label, i) => { + const param = `excludeLabels_${i}` + qb.andWhere( + `array_to_string(array_cat(library_item.label_names, library_item.highlight_labels)::text[], ',') NOT ILIKE :${param}`, + { + [param]: label.replace(/\*/g, '%'), + } + ) + }) + }) + ) + } else { + queryBuilder.andWhere( + 'NOT lower(array_cat(library_item.label_names, library_item.highlight_labels)::text)::text[] && ARRAY[:...excludeLabels]::text[]', + { + excludeLabels: labels, + } + ) + } } } diff --git a/packages/api/test/resolvers/article.test.ts b/packages/api/test/resolvers/article.test.ts index e5fb2d5ee..dda47a53f 100644 --- a/packages/api/test/resolvers/article.test.ts +++ b/packages/api/test/resolvers/article.test.ts @@ -1208,6 +1208,61 @@ describe('Article API', () => { }) }) + context('when wildcard search for labels', () => { + let items: LibraryItem[] = [] + let labelIds: string[] + + before(async () => { + keyword = 'label:test/*' + // Create some test items + const label1 = await createLabel('test/one', '', user.id) + const label2 = await createLabel('test/two', '', user.id) + labelIds = [label1.id, label2.id] + + items = await createLibraryItems( + [ + { + user, + title: 'test title wildcard', + readableContent: '
test wildcard
', + slug: 'test slug wildcard', + originalUrl: `${url}/wildcard`, + }, + { + user, + title: 'test title wildcard 1', + readableContent: 'test wildcard
', + slug: 'test slug wildcard 1', + originalUrl: `${url}/wildcard_1`, + }, + { + user, + title: 'test title wildcard 2', + readableContent: 'test wildcard
', + slug: 'test slug wildcard 2', + originalUrl: `${url}/wildcard_2`, + }, + ], + user.id + ) + await saveLabelsInLibraryItem([label1], items[0].id, user.id) + await saveLabelsInLibraryItem([label2], items[1].id, user.id) + }) + + after(async () => { + await deleteLabels(labelIds, user.id) + await deleteLibraryItems(items, user.id) + }) + + it('returns library items with label test/one and test/two', 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[0].id) + expect(res.body.data.search.edges[1].node.id).to.eq(items[1].id) + }) + }) + context('when type:file label:test is in the query', () => { let items: LibraryItem[] = [] let label: Label