From 711536c12c8636a343a29a2b912fc5be3f2f09e3 Mon Sep 17 00:00:00 2001 From: Sebastian Jeltsch Date: Sun, 8 Dec 2024 00:50:04 +0100 Subject: [PATCH] Use URLSearchParams for params construction and fix filter splitting in JS. --- client/trailbase-ts/README.md | 18 ++++++++-------- client/trailbase-ts/package.json | 2 +- client/trailbase-ts/src/index.ts | 21 +++++++++---------- .../tests/integration/integration.test.ts | 14 ++++++++----- examples/coffeesearch/src/App.tsx | 15 ++++++++----- 5 files changed, 39 insertions(+), 31 deletions(-) diff --git a/client/trailbase-ts/README.md b/client/trailbase-ts/README.md index 8175978e..95ff22af 100644 --- a/client/trailbase-ts/README.md +++ b/client/trailbase-ts/README.md @@ -1,14 +1,14 @@ -# JS/TS Client for TrailBase +# TrailBase Client TrailBase is a [blazingly](https://trailbase.io/reference/benchmarks/) fast, -single-file, open-source application server with type-safe APIs, built-in -JS/ES6/TS Runtime, Auth, and Admin UI built on Rust+SQLite+V8. +open-source application server with type-safe APIs, built-in JS/ES6/TS Runtime, +Auth, and Admin UI built on Rust, SQLite & V8. -For more context, documentation, and an online demo, check out our website +For more context, documentation, and an online demo, check out the website: [trailbase.io](https://trailbase.io). -This is the first-party client for hooking up your PWA, SPAs and JS backend -applications with TrailBase. -While we're working on better documentation, an example setup can be found under -[`/examples/blog/web`](https://github.com/trailbaseio/trailbase/tree/main/examples/blog/web) -in the repository. +This is the first-party client for hooking up your web and headless (node.js, +deno, ...) apps with TrailBase. +While we're working on better documentation, an example web app can be found in +the repository under: +[`/examples/blog/web`](https://github.com/trailbaseio/trailbase/tree/main/examples/blog/web). diff --git a/client/trailbase-ts/package.json b/client/trailbase-ts/package.json index 5ec22d2c..0e202db9 100644 --- a/client/trailbase-ts/package.json +++ b/client/trailbase-ts/package.json @@ -1,7 +1,7 @@ { "name": "trailbase", "description": "Official TrailBase client", - "version": "0.1.2", + "version": "0.1.3", "license": "OSL-3.0", "type": "module", "main": "./src/index.ts", diff --git a/client/trailbase-ts/src/index.ts b/client/trailbase-ts/src/index.ts index 02ca8dcf..1d0d277a 100644 --- a/client/trailbase-ts/src/index.ts +++ b/client/trailbase-ts/src/index.ts @@ -115,34 +115,33 @@ export class RecordApi { order?: string[]; filters?: string[]; }): Promise { - const params: [string, string][] = []; + const params = new URLSearchParams(); const pagination = opts?.pagination; if (pagination) { const cursor = pagination.cursor; - if (cursor) params.push(["cursor", cursor]); + if (cursor) params.append("cursor", cursor); const limit = pagination.limit; - if (limit) params.push(["limit", limit.toString()]); + if (limit) params.append("limit", limit.toString()); } const order = opts?.order; - if (order) params.push(["order", order.join(",")]); + if (order) params.append("order", order.join(",")); const filters = opts?.filters; if (filters) { for (const filter of filters) { - const [nameOp, value] = filter.split("=", 2); - if (value === undefined) { + const pos = filter.indexOf("="); + if (pos <= 0) { throw Error(`Filter '${filter}' does not match: 'name[op]=value'`); } - params.push([nameOp, value]); + const nameOp = filter.slice(0, pos); + const value = filter.slice(pos + 1); + params.append(nameOp, value); } } - const queryParams = encodeURI( - params.map(([key, value]) => `${key}=${value}`).join("&"), - ); const response = await this.client.fetch( - `${RecordApi._recordApi}/${this.name}?${queryParams}`, + `${RecordApi._recordApi}/${this.name}?${params}`, ); return (await response.json()) as T[]; } diff --git a/client/trailbase-ts/tests/integration/integration.test.ts b/client/trailbase-ts/tests/integration/integration.test.ts index e2cd3a19..5b7a6a9c 100644 --- a/client/trailbase-ts/tests/integration/integration.test.ts +++ b/client/trailbase-ts/tests/integration/integration.test.ts @@ -71,7 +71,11 @@ test("Record integration tests", async () => { const api = client.records("simple_strict_table"); const now = new Date().getTime(); - const messages = [`ts client test 0: ${now}`, `ts client test 1: ${now}`]; + // Throw in some url characters for good measure. + const messages = [ + `ts client test 1: =?&${now}`, + `ts client test 2: =?&${now}`, + ]; const ids: string[] = []; for (const msg of messages) { @@ -90,7 +94,7 @@ test("Record integration tests", async () => { { const records = await api.list({ - filters: [`text_not_null[like]=%${now}`], + filters: [`text_not_null[like]=% =?&${now}`], order: ["+text_not_null"], }); expect(records.map((el) => el.text_not_null)).toStrictEqual(messages); @@ -157,7 +161,7 @@ test("record error tests", async () => { String.fromCharCode.apply(null, uuidParse(uuidv7())), ); const nonExistantApi = client.records("non-existant"); - expect( + await expect( async () => await nonExistantApi.read(nonExistantId), ).rejects.toThrowError( expect.objectContaining({ @@ -166,14 +170,14 @@ test("record error tests", async () => { ); const api = client.records("simple_strict_table"); - expect( + await expect( async () => await api.read("invalid id"), ).rejects.toThrowError( expect.objectContaining({ status: status.BAD_REQUEST, }), ); - expect( + await expect( async () => await api.read(nonExistantId), ).rejects.toThrowError( expect.objectContaining({ diff --git a/examples/coffeesearch/src/App.tsx b/examples/coffeesearch/src/App.tsx index 8cb9c8bf..601ec561 100644 --- a/examples/coffeesearch/src/App.tsx +++ b/examples/coffeesearch/src/App.tsx @@ -25,12 +25,17 @@ function Table() { const [acidity, setAcidity] = useState(8); const [sweetness, setSweetness] = useState(8); - const [data, setData] = useState> | undefined>(); + type Record = [string, number, number, number, number]; + const [data, setData] = useState([]); + useEffect(() => { const URL = import.meta.env.DEV ? "http://localhost:4000" : ""; - const params = Object.entries({ aroma, flavor, acidity, sweetness }) - .map(([k, v]) => `${k}=${v}`) - .join("&"); + const params = new URLSearchParams({ + aroma: `${aroma}`, + flavor: `${flavor}`, + acidity: `${acidity}`, + sweetness: `${sweetness}`, + }); fetch(`${URL}/search?${params}`).then(async (r) => setData(await r.json())); }, [aroma, flavor, acidity, sweetness]); @@ -57,7 +62,7 @@ function Table() { - {(data ?? []).map((row) => ( + {data.map((row) => ( {row.map((d) => ( {d.toString()}