From b53757ea25098d094117f7cc530e66b884264d2f Mon Sep 17 00:00:00 2001 From: Jack Singer Date: Thu, 16 Jul 2026 14:34:01 -0500 Subject: [PATCH] =?UTF-8?q?entities:=20raise=20list/filter=20row=20cap=201?= =?UTF-8?q?000=20=E2=86=92=205000=20(Base44=20parity);=20v0.2.0-next.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit list/filter still page (50 by default) but the per-call ceiling was 1000 while Base44's is 5000, so a Base44-modeled app silently truncated between 1000–5000 rows. Match 5000, and THROW on an over-cap limit instead of silently clamping — over-large reads now fail loudly rather than returning a partial result the caller mistakes for the whole set. Page larger tables with limit + skip. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 8 ++++++++ README.md | 3 +++ package.json | 2 +- src/entities.test.ts | 22 ++++++++++++++++++---- src/entities.ts | 18 +++++++++++++----- 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60fef50..3f11e55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.2.0-next.7 + +- **Entities pagination cap raised 1000 → 5000, matching Base44.** `list` and + `filter` still page (50 rows by default) but now allow up to 5000 rows per + call. A `limit` above the cap **throws** instead of silently truncating, so + over-large reads fail loudly rather than returning a partial result the caller + mistakes for the whole set. Page larger tables with `limit` + `skip`. + ## 0.2.0-next.6 Combines the entities data layer (next.0–next.4) with the auth fail-safe fix diff --git a/README.md b/README.md index 020097c..72a4ce6 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,9 @@ tested, and upgradable independently of any one app. ``` Filters use MongoDB-style operators (`$eq $ne $gt $gte $lt $lte $in $nin $exists $regex $all $not`, plus `$and`/`$or`/`$nor`); sort is a `-col` string. + `list`/`filter` are paged: **50 rows by default, 5000 max per call** (over-cap + throws) — page larger tables with the `limit` + `skip` args. `updateMany` / + `deleteMany` act on every matching row regardless of page size. - **Data + Storage through the Bool gateway.** `client.db` is a standard [supabase-js](https://supabase.com/docs/reference/javascript) client (what `entities` is built on) whose REST and Storage traffic is routed to the Bool diff --git a/package.json b/package.json index 4440a4f..d015f1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bool-sdk", - "version": "0.2.0-next.6", + "version": "0.2.0-next.7", "description": "Client SDK for apps built on Bool — gateway data access, end-user auth, and the React auth layer.", "type": "module", "main": "./dist/index.js", diff --git a/src/entities.test.ts b/src/entities.test.ts index bf93402..2d53b00 100644 --- a/src/entities.test.ts +++ b/src/entities.test.ts @@ -145,11 +145,25 @@ describe("entities: read paths → PostgREST", () => { expect(reqQuery(calls[0]!).get("order")).toBe("title.asc"); }); - test("list caps the limit at 1000", async () => { + test("list() allows an explicit limit up to the max (5000)", async () => { const bool = createBoolClient(CONFIG); - await bool.entities.todos.list("-created_at", 99999); - // range(0, 999) → limit 1000 - expect(reqQuery(calls[0]!).get("limit")).toBe("1000"); + await bool.entities.todos.list("-created_at", 5000); + expect(reqQuery(calls[0]!).get("limit")).toBe("5000"); + }); + + test("list() throws on an over-cap limit instead of silently truncating", async () => { + const bool = createBoolClient(CONFIG); + await expect(bool.entities.todos.list("-created_at", 5001)).rejects.toThrow(/maximum/i); + // It fails before touching the network — no partial/truncated request goes out. + expect(calls).toHaveLength(0); + }); + + test("filter() defaults to the same 50-row page", async () => { + const bool = createBoolClient(CONFIG); + await bool.entities.todos.filter({ status: "active" }); + const q = reqQuery(calls[0]!); + expect(q.get("status")).toBe("eq.active"); + expect(q.get("limit")).toBe("50"); }); test("fields limits the selected columns", async () => { diff --git a/src/entities.ts b/src/entities.ts index 4b2e238..89f3120 100644 --- a/src/entities.ts +++ b/src/entities.ts @@ -86,8 +86,9 @@ export type ImportResult = { /** CRUD + realtime for one entity table. `T` defaults to `any` (untyped); * apps can pass a row type for autocomplete. Mirrors Base44's EntityHandler. */ export interface EntityHandler { - /** All rows, newest first by default. `limit` defaults to 50, max 1000. - * `fields` restricts the columns returned. */ + /** Rows, newest first by default. `limit` defaults to 50, max 5000 — page + * through larger result sets with `limit` + `skip` (an over-cap `limit` + * throws rather than silently truncating). `fields` restricts the columns. */ list(sort?: SortSpec, limit?: number, skip?: number, fields?: (keyof T & string)[]): Promise; /** Rows matching `query`. See {@link FilterQuery}. */ filter( @@ -145,7 +146,7 @@ export interface EntitiesModule { const DEFAULT_SORT = "-created_at"; const DEFAULT_LIMIT = 50; -const MAX_LIMIT = 1000; +const MAX_LIMIT = 5000; // A supabase-js filter/query builder is chainable and thenable; we only need // the handful of methods below, so keep it loosely typed rather than importing @@ -346,8 +347,15 @@ function createEntityHandler( return fields && fields.length ? fields.join(",") : "*"; } function paginate(query: QueryBuilder, limit: number, skip: number): QueryBuilder { - const capped = Math.min(limit, MAX_LIMIT); - return query.range(skip, skip + capped - 1); + // A single request returns at most MAX_LIMIT rows. Refuse an over-cap limit + // loudly rather than silently truncating — page through with limit + skip. + if (limit > MAX_LIMIT) { + throw new Error( + `limit ${limit} exceeds the ${MAX_LIMIT}-row maximum per request; ` + + `page through larger result sets with limit + skip.`, + ); + } + return query.range(skip, skip + limit - 1); } async function unwrap(query: QueryBuilder): Promise { const { data, error } = await query;