Something went wrong. Try again.
A decentralized music tracking and discovery platform built on AT Protocol 🎵 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160import { describe, expect, it } from "bun:test";import type { SQL } from "drizzle-orm";import { PgDialect } from "drizzle-orm/pg-core";import tables from "schema";import { compileRsqlFilter, type RsqlFieldMap, RsqlFilterError, rsqlSelectors,} from "./rsql";
const FIELDS: RsqlFieldMap = { title: tables.tracks.title, artist: tables.tracks.artist, uri: tables.tracks.uri, genre: tables.tracks.genre, duration: { column: tables.tracks.duration, type: "number" }, createdAt: { column: tables.tracks.createdAt, type: "date" }, genres: { column: tables.artists.genres, type: "string[]" },};
const dialect = new PgDialect();const render = (expr: SQL) => dialect.sqlToQuery(expr);const compile = (filter: string) => render(compileRsqlFilter(filter, FIELDS));
describe("compileRsqlFilter", () => { it("compiles equality to a parameterized comparison", () => { const q = compile('title=="Discovery"'); expect(q.sql).toContain('"title" = '); expect(q.params).toEqual(["Discovery"]); });
it("compiles inequality", () => { const q = compile("artist!=Eminem"); expect(q.sql).toContain("<>"); expect(q.params).toEqual(["Eminem"]); });
it("compiles and/or with grouping", () => { const q = compile("(genre==house,genre==electro);duration=gt=200000"); expect(q.sql).toContain(" or "); expect(q.sql).toContain(" and "); expect(q.params).toEqual(["house", "electro", 200000]); });
it("supports verbose and/or combinators", () => { const q = compile("genre==house or genre==electro"); expect(q.sql).toContain(" or "); expect(q.params).toEqual(["house", "electro"]); });
it("translates * wildcards to case-insensitive like", () => { const q = compile("artist==Daft*"); expect(q.sql.toLowerCase()).toContain("like"); expect(q.params).toEqual(["Daft%"]); expect(q.sql).toContain('lower("tracks"."artist")'); });
it("escapes like metacharacters inside wildcard values", () => { const q = compile('artist=="*100%_*"'); expect(q.params).toEqual(["%100\\%\\_%"]); });
it("compiles negated wildcard to indexed lower LIKE", () => { const q = compile("artist!=Daft*"); expect(q.sql.toLowerCase()).toContain("not"); expect(q.sql.toLowerCase()).toContain("like"); expect(q.params).toEqual(["Daft%"]); });
it("compiles =in= and =out=", () => { const q = compile("genre=in=(house,electro)"); expect(q.sql).toContain(" in "); expect(q.params).toEqual(["house", "electro"]);
const nq = compile("genre=out=(house,electro)"); expect(nq.sql).toContain(" not in "); expect(nq.params).toEqual(["house", "electro"]); });
it("compiles ordered comparisons with number coercion", () => { const q = compile("duration>=200000"); expect(q.sql).toContain(">="); expect(q.params).toEqual([200000]); });
it("coerces date fields to Date values", () => { const q = compileRsqlFilter("createdAt=ge=2025-01-01", FIELDS); const rendered = render(q); expect(rendered.params).toHaveLength(1); });
it("rejects non-numeric values on number fields", () => { expect(() => compile("duration=gt=abc")).toThrow(RsqlFilterError); });
it("rejects invalid dates on date fields", () => { expect(() => compile("createdAt=lt=not-a-date")).toThrow(RsqlFilterError); });
it("compiles string[] equality to array containment", () => { const q = compile("genres==house"); expect(q.sql).toContain("@>"); expect(q.params).toEqual(["house"]); });
it("compiles negated string[] equality to negated containment", () => { const q = compile("genres!=house"); expect(q.sql).toContain("not"); expect(q.sql).toContain("@>"); expect(q.params).toEqual(["house"]); });
it("compiles string[] =in= to array overlap", () => { const q = compile("genres=in=(house,electro)"); expect(q.sql).toContain("&&"); expect(q.params).toEqual(["house", "electro"]); });
it("rejects ordered comparisons on string[] fields", () => { expect(() => compile("genres=gt=house")).toThrow(RsqlFilterError); });
it("compiles null equality to is null / is not null", () => { expect(compile("uri==null").sql).toContain("is null"); expect(compile("uri!=null").sql).toContain("is not null"); });
it("rejects unknown fields and lists the allowed ones", () => { expect(() => compile("password==hunter2")).toThrow( /Unknown filter field "password".*artist.*title/s, ); });
it("rejects malformed expressions", () => { expect(() => compile("title==")).toThrow(RsqlFilterError); expect(() => compile("title")).toThrow(RsqlFilterError); });});
describe("rsqlSelectors", () => { it("collects the selectors of a compound expression", () => { expect( rsqlSelectors('name=="Road trip";track.artist=="Daft Punk"').sort(), ).toEqual(["name", "track.artist"]); });
it("deduplicates repeated selectors", () => { expect(rsqlSelectors("track.artist==Air,track.artist==Phoenix")).toEqual([ "track.artist", ]); });
it("returns nothing for an absent or unparseable filter", () => { expect(rsqlSelectors(undefined)).toEqual([]); expect(rsqlSelectors(" ")).toEqual([]); expect(rsqlSelectors("title==")).toEqual([]); });});