mirror of
https://github.com/karakeep-app/karakeep.git
synced 2025-12-12 20:35:52 +01:00
feat: add is:broken search qualifier for broken links
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
This commit is contained in:
@@ -63,6 +63,12 @@ const QUALIFIER_DEFINITIONS = [
|
||||
value: "is:media",
|
||||
appendSpace: true,
|
||||
},
|
||||
{
|
||||
value: "is:broken",
|
||||
descriptionKey: "search.is_broken_link",
|
||||
negatedDescriptionKey: "search.is_not_broken_link",
|
||||
appendSpace: true,
|
||||
},
|
||||
{
|
||||
value: "url:",
|
||||
descriptionKey: "search.url_contains",
|
||||
|
||||
@@ -662,6 +662,8 @@
|
||||
"type_is_not": "Type is not",
|
||||
"is_from_feed": "Is from RSS Feed",
|
||||
"is_not_from_feed": "Is not from RSS Feed",
|
||||
"is_broken_link": "Has Broken Link",
|
||||
"is_not_broken_link": "Has Working Link",
|
||||
"and": "And",
|
||||
"or": "Or",
|
||||
"history": "Recent Searches",
|
||||
|
||||
@@ -20,6 +20,7 @@ Here's a comprehensive table of all supported qualifiers:
|
||||
| `is:tagged` | Bookmarks that has one or more tags | `is:tagged` |
|
||||
| `is:inlist` | Bookmarks that are in one or more lists | `is:inlist` |
|
||||
| `is:link`, `is:text`, `is:media` | Bookmarks that are of type link, text or media | `is:link` |
|
||||
| `is:broken` | Bookmarks with broken/failed links (crawl failures or non-2xx status codes) | `is:broken` |
|
||||
| `url:<value>` | Match bookmarks with URL substring | `url:example.com` |
|
||||
| `title:<value>` | Match bookmarks with title substring | `title:example` |
|
||||
| | Supports quoted strings for titles with spaces | `title:"my title"` |
|
||||
|
||||
@@ -123,6 +123,22 @@ describe("Search Query Parser", () => {
|
||||
inverse: true,
|
||||
},
|
||||
});
|
||||
expect(parseSearchQuery("is:broken")).toEqual({
|
||||
result: "full",
|
||||
text: "",
|
||||
matcher: {
|
||||
type: "brokenLinks",
|
||||
brokenLinks: true,
|
||||
},
|
||||
});
|
||||
expect(parseSearchQuery("-is:broken")).toEqual({
|
||||
result: "full",
|
||||
text: "",
|
||||
matcher: {
|
||||
type: "brokenLinks",
|
||||
brokenLinks: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("simple string queries", () => {
|
||||
|
||||
@@ -166,6 +166,11 @@ MATCHER.setPattern(
|
||||
inverse: !!minus,
|
||||
},
|
||||
};
|
||||
case "broken":
|
||||
return {
|
||||
text: "",
|
||||
matcher: { type: "brokenLinks", brokenLinks: !minus },
|
||||
};
|
||||
default:
|
||||
// If the token is not known, emit it as pure text
|
||||
return {
|
||||
|
||||
@@ -83,6 +83,11 @@ const zTypeMatcher = z.object({
|
||||
inverse: z.boolean(),
|
||||
});
|
||||
|
||||
const zBrokenLinksMatcher = z.object({
|
||||
type: z.literal("brokenLinks"),
|
||||
brokenLinks: z.boolean(),
|
||||
});
|
||||
|
||||
const zNonRecursiveMatcher = z.union([
|
||||
zTagNameMatcher,
|
||||
zListNameMatcher,
|
||||
@@ -97,6 +102,7 @@ const zNonRecursiveMatcher = z.union([
|
||||
zIsInListMatcher,
|
||||
zTypeMatcher,
|
||||
zRssFeedNameMatcher,
|
||||
zBrokenLinksMatcher,
|
||||
]);
|
||||
|
||||
type NonRecursiveMatcher = z.infer<typeof zNonRecursiveMatcher>;
|
||||
@@ -120,6 +126,7 @@ export const zMatcherSchema: z.ZodType<Matcher> = z.lazy(() => {
|
||||
zIsInListMatcher,
|
||||
zTypeMatcher,
|
||||
zRssFeedNameMatcher,
|
||||
zBrokenLinksMatcher,
|
||||
z.object({
|
||||
type: z.literal("and"),
|
||||
matchers: z.array(zMatcherSchema),
|
||||
|
||||
@@ -350,6 +350,29 @@ async function getIds(
|
||||
),
|
||||
);
|
||||
}
|
||||
case "brokenLinks": {
|
||||
// Only applies to bookmarks of type LINK
|
||||
return db
|
||||
.select({ id: bookmarkLinks.id })
|
||||
.from(bookmarkLinks)
|
||||
.leftJoin(bookmarks, eq(bookmarks.id, bookmarkLinks.id))
|
||||
.where(
|
||||
and(
|
||||
eq(bookmarks.userId, userId),
|
||||
matcher.brokenLinks
|
||||
? or(
|
||||
eq(bookmarkLinks.crawlStatus, "failure"),
|
||||
lt(bookmarkLinks.crawlStatusCode, 200),
|
||||
gt(bookmarkLinks.crawlStatusCode, 299),
|
||||
)
|
||||
: and(
|
||||
eq(bookmarkLinks.crawlStatus, "success"),
|
||||
gte(bookmarkLinks.crawlStatusCode, 200),
|
||||
lte(bookmarkLinks.crawlStatusCode, 299),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
case "and": {
|
||||
const vals = await Promise.all(
|
||||
matcher.matchers.map((m) => getIds(db, userId, m)),
|
||||
|
||||
Reference in New Issue
Block a user