Repository navigation
fix(aggregator): match a partly typed last search word as a prefix #3960
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -686,6 +686,29 @@ describe("searchPackages", () => { | |||||||||||||||||||||||||
| expect(overlap).toEqual([]); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| it("matches a partly typed last word as a prefix", async () => { | ||||||||||||||||||||||||||
| await seedPackage({ slug: "bulletin", name: "Bulletin", description: "Email newsletters" }); | ||||||||||||||||||||||||||
| await seedPackage({ slug: "gallery", name: "Gallery", description: "Image gallery" }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const res = await SELF.fetch( | ||||||||||||||||||||||||||
| `https://test/xrpc/${NSID.aggregatorSearchPackages}?q=${encodeURIComponent("email newslet")}`, | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| const body = (await res.json()) as { packages: Array<{ slug: string }> }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| expect(body.packages.map((p) => p.slug)).toEqual(["bulletin"]); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| it("returns no matches for a query with no word characters", async () => { | ||||||||||||||||||||||||||
| await seedPackage({ slug: "demo", name: "Demo" }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const res = await SELF.fetch( | ||||||||||||||||||||||||||
| `https://test/xrpc/${NSID.aggregatorSearchPackages}?q=${encodeURIComponent('( * "')}`, | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| expect(res.status).toBe(200); | ||||||||||||||||||||||||||
| await expect(res.json()).resolves.toEqual({ packages: [] }); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| it("doesn't blow up on FTS-unsafe query chars (defensive quoting)", async () => { | ||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] The new regression test validates prefix matching inside a multi-word query (
Suggested change
|
||||||||||||||||||||||||||
| await seedPackage({ slug: "demo", name: "Demo" }); | ||||||||||||||||||||||||||
| const res = await SELF.fetch( | ||||||||||||||||||||||||||
|
|
@@ -699,10 +722,10 @@ describe("searchPackages", () => { | |||||||||||||||||||||||||
| await seedPackage({ slug: "alpha", name: "Alpha" }); | ||||||||||||||||||||||||||
| await seedPackage({ slug: "beta", name: "Beta" }); | ||||||||||||||||||||||||||
| // `alpha OR beta` would match both packages if `OR` were interpreted | ||||||||||||||||||||||||||
| // as the FTS5 operator. With proper escaping the whole string is one | ||||||||||||||||||||||||||
| // literal phrase that can't possibly appear in either record's | ||||||||||||||||||||||||||
| // indexed text → zero matches. A buggy escape that stripped the | ||||||||||||||||||||||||||
| // quotes would return *both* packages. | ||||||||||||||||||||||||||
| // as the FTS5 operator. With proper escaping every term is a literal | ||||||||||||||||||||||||||
| // that must appear, and neither record contains all three → zero | ||||||||||||||||||||||||||
| // matches. A buggy escape that stripped the quotes would return | ||||||||||||||||||||||||||
| // *both* packages. | ||||||||||||||||||||||||||
| const res = await SELF.fetch( | ||||||||||||||||||||||||||
| `https://test/xrpc/${NSID.aggregatorSearchPackages}?q=${encodeURIComponent("alpha OR beta")}`, | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[suggestion] The function appends its own
*after the last quoted term, but it doesn't remove user-supplied*characters from the term itself. An input likefoo*would produce"foo*"*— a literal*inside a phrase plus a phrase-level prefix*. I can't run SQLite here to verify, but that looks like a potential FTS5 syntax error and would weaken the existing "FTS-unsafe chars don't 500" guarantee. Since the code already owns prefixing, consider stripping*from terms before quoting.