Skip to content

Commit 1f43f23

Browse files
feat: add is:broken search qualifier for broken links (#2225)
Add a new search qualifier `is:broken` that allows users to filter bookmarks with broken or failed links. This matches the functionality on the broken links settings page, where a link is considered broken if: - crawlStatus is "failure" - crawlStatusCode is less than 200 - crawlStatusCode is greater than 299 The qualifier supports negation with `-is:broken` to find working links. Changes: - Add brokenLinks matcher type definition - Update search query parser to handle is:broken qualifier - Implement query execution logic for broken links filtering - Add autocomplete support with translations - Add parser tests - Update search query language documentation Co-authored-by: Claude <[email protected]>
1 parent 13a090c commit 1f43f23

File tree

7 files changed

+60
-0
lines changed

7 files changed

+60
-0
lines changed

apps/web/components/dashboard/search/useSearchAutocomplete.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ const QUALIFIER_DEFINITIONS = [
6363
value: "is:media",
6464
appendSpace: true,
6565
},
66+
{
67+
value: "is:broken",
68+
descriptionKey: "search.is_broken_link",
69+
negatedDescriptionKey: "search.is_not_broken_link",
70+
appendSpace: true,
71+
},
6672
{
6773
value: "url:",
6874
descriptionKey: "search.url_contains",

apps/web/lib/i18n/locales/en/translation.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,8 @@
662662
"type_is_not": "Type is not",
663663
"is_from_feed": "Is from RSS Feed",
664664
"is_not_from_feed": "Is not from RSS Feed",
665+
"is_broken_link": "Has Broken Link",
666+
"is_not_broken_link": "Has Working Link",
665667
"and": "And",
666668
"or": "Or",
667669
"history": "Recent Searches",

docs/docs/14-guides/02-search-query-language.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Here's a comprehensive table of all supported qualifiers:
2020
| `is:tagged` | Bookmarks that has one or more tags | `is:tagged` |
2121
| `is:inlist` | Bookmarks that are in one or more lists | `is:inlist` |
2222
| `is:link`, `is:text`, `is:media` | Bookmarks that are of type link, text or media | `is:link` |
23+
| `is:broken` | Bookmarks with broken/failed links (crawl failures or non-2xx status codes) | `is:broken` |
2324
| `url:<value>` | Match bookmarks with URL substring | `url:example.com` |
2425
| `title:<value>` | Match bookmarks with title substring | `title:example` |
2526
| | Supports quoted strings for titles with spaces | `title:"my title"` |

packages/shared/searchQueryParser.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,22 @@ describe("Search Query Parser", () => {
123123
inverse: true,
124124
},
125125
});
126+
expect(parseSearchQuery("is:broken")).toEqual({
127+
result: "full",
128+
text: "",
129+
matcher: {
130+
type: "brokenLinks",
131+
brokenLinks: true,
132+
},
133+
});
134+
expect(parseSearchQuery("-is:broken")).toEqual({
135+
result: "full",
136+
text: "",
137+
matcher: {
138+
type: "brokenLinks",
139+
brokenLinks: false,
140+
},
141+
});
126142
});
127143

128144
test("simple string queries", () => {

packages/shared/searchQueryParser.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,11 @@ MATCHER.setPattern(
166166
inverse: !!minus,
167167
},
168168
};
169+
case "broken":
170+
return {
171+
text: "",
172+
matcher: { type: "brokenLinks", brokenLinks: !minus },
173+
};
169174
default:
170175
// If the token is not known, emit it as pure text
171176
return {

packages/shared/types/search.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ const zTypeMatcher = z.object({
8383
inverse: z.boolean(),
8484
});
8585

86+
const zBrokenLinksMatcher = z.object({
87+
type: z.literal("brokenLinks"),
88+
brokenLinks: z.boolean(),
89+
});
90+
8691
const zNonRecursiveMatcher = z.union([
8792
zTagNameMatcher,
8893
zListNameMatcher,
@@ -97,6 +102,7 @@ const zNonRecursiveMatcher = z.union([
97102
zIsInListMatcher,
98103
zTypeMatcher,
99104
zRssFeedNameMatcher,
105+
zBrokenLinksMatcher,
100106
]);
101107

102108
type NonRecursiveMatcher = z.infer<typeof zNonRecursiveMatcher>;
@@ -120,6 +126,7 @@ export const zMatcherSchema: z.ZodType<Matcher> = z.lazy(() => {
120126
zIsInListMatcher,
121127
zTypeMatcher,
122128
zRssFeedNameMatcher,
129+
zBrokenLinksMatcher,
123130
z.object({
124131
type: z.literal("and"),
125132
matchers: z.array(zMatcherSchema),

packages/trpc/lib/search.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,29 @@ async function getIds(
350350
),
351351
);
352352
}
353+
case "brokenLinks": {
354+
// Only applies to bookmarks of type LINK
355+
return db
356+
.select({ id: bookmarkLinks.id })
357+
.from(bookmarkLinks)
358+
.leftJoin(bookmarks, eq(bookmarks.id, bookmarkLinks.id))
359+
.where(
360+
and(
361+
eq(bookmarks.userId, userId),
362+
matcher.brokenLinks
363+
? or(
364+
eq(bookmarkLinks.crawlStatus, "failure"),
365+
lt(bookmarkLinks.crawlStatusCode, 200),
366+
gt(bookmarkLinks.crawlStatusCode, 299),
367+
)
368+
: and(
369+
eq(bookmarkLinks.crawlStatus, "success"),
370+
gte(bookmarkLinks.crawlStatusCode, 200),
371+
lte(bookmarkLinks.crawlStatusCode, 299),
372+
),
373+
),
374+
);
375+
}
353376
case "and": {
354377
const vals = await Promise.all(
355378
matcher.matchers.map((m) => getIds(db, userId, m)),

0 commit comments

Comments
 (0)