Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,50 @@
# Changelog

## 0.2.0

Adds the **entities data layer** — a Base44-parity data API over the gateway so
apps read/write data without touching Supabase, SQL, or credentials directly:

```ts
const todos = await bool.entities.todos.list("-created_at");
const one = await bool.entities.todos.create({ title: "hi" });
await bool.entities.todos.update(one.id, { done: true });
await bool.entities.todos.filter({ status: "active", count: { $gte: 10 } });
```

`bool.entities.<table>` mirrors Base44's entity surface one-to-one:
- **Reads:** `list`, `filter`, `get` — with `sort` (`-col`), `limit`, `skip`,
and `fields` (column selection).
- **Writes:** `create`, `bulkCreate`, `update`, `bulkUpdate`, `delete`.
- **Bulk-by-query:** `updateMany(query, { $set })`, `deleteMany(query)`.
- **Import:** `importEntities(csvFile)` (parsed client-side → `bulkCreate`).
- **Realtime:** `subscribe(cb)` (gateway doorbell).
- **Filter DSL:** MongoDB-style — `$eq $ne $gt $gte $lt $lte $in $nin $exists
$regex $all $not` per field, `$and`/`$or`/`$nor` at the root, array shorthand,
and `null` → IS NULL.

Methods return row data directly and throw on error. Additive and
backward-compatible — `bool.db` / `supabase` still work.

Known gaps vs. Base44 (documented, follow-ups): `updateMany` with
`$inc/$mul/$push/$pull` is read-modify-write (not atomic under concurrent
writers — a Postgres RPC would make it atomic); `$size` (filter by array
length) isn't expressible over PostgREST and is omitted.

**`EntitiesModule` is now an augmentable `interface`** (was a `type` alias), so
generated apps can type each entity via `declare module "bool-sdk"`:

```ts
declare module "bool-sdk" {
interface EntitiesModule { board_games: EntityHandler<BoardGames> }
}
```

That makes `bool.entities.board_games` typed (field names, enum values, types)
while the string index signature keeps un-declared tables usable as
`EntityHandler<any>`. Bool's `define_entity` tool writes one such `.d.ts` per
model. No runtime change.

## 0.1.1

Publishing now goes through npm OIDC trusted publishing (no long-lived token).
Expand Down
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,26 @@ tested, and upgradable independently of any one app.

## What it does

- **Entities data API.** `client.entities.<table>` is the recommended way to
read/write data — a one-to-one mirror of Base44's entity surface: `list`,
`filter`, `get`, `create`, `bulkCreate`, `update`, `bulkUpdate`, `updateMany`,
`delete`, `deleteMany`, `importEntities`, `subscribe`. It hides Supabase/SQL
entirely; methods return rows directly and throw on error:
```ts
const todos = await bool.entities.todos.list("-created_at");
const one = await bool.entities.todos.create({ title: "hi" });
await bool.entities.todos.update(one.id, { done: true });
await bool.entities.todos.filter({ status: "active", count: { $gte: 10 } });
await bool.entities.todos.updateMany({ done: false }, { $set: { done: true } });
```
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.
- **Data + Storage through the Bool gateway.** `client.db` is a standard
[supabase-js](https://supabase.com/docs/reference/javascript) client whose
REST and Storage traffic is routed to the Bool gateway (`/_bool/v1/db`). The
gateway injects the real credential server-side and pins the app's private
Postgres schema — the anon key in the bundle has no data grants and can't
read anything directly.
[supabase-js](https://supabase.com/docs/reference/javascript) client (what
`entities` is built on) whose REST and Storage traffic is routed to the Bool
gateway (`/_bool/v1/db`). The gateway injects the real credential server-side
and pins the app's private Postgres schema — the anon key in the bundle has
no data grants and can't read anything directly.
- **Realtime "doorbell".** Postgres changes broadcast a row-data-free
`{table, op}` ping on the app's public channel; `subscribeToChanges` wraps
the subscription. Refetch on each ping — the ping never carries row data.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bool-sdk",
"version": "0.1.1",
"version": "0.2.0-next.4",
"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",
Expand Down
41 changes: 25 additions & 16 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
// Keep this in sync with the gateway data route (/_bool/v1/db) and users route
// (/_bool/v1/users) in the Bool platform repo.
import { createClient, type SupabaseClient } from "@supabase/supabase-js";
import { createEntitiesModule, type EntitiesModule } from "./entities.js";

/** Matches the server's append-only gateway path version. */
const GATEWAY_API = "v1";
Expand Down Expand Up @@ -99,6 +100,9 @@ export type BoolClient = {
* Use it exactly like a normal supabase-js client: `.from(...)`, `.storage`,
* `.channel(...)`. Do NOT use `db.auth` — end-user auth is `client.auth`. */
db: BoolDb;
/** The data API: `entities.<table>.list/filter/get/create/update/delete`.
* The recommended way to read/write app data — hides Supabase entirely. */
entities: EntitiesModule;
/** End-user auth for this app (gateway users plane). */
auth: BoolAuth;
/** This app's private Postgres schema name. */
Expand Down Expand Up @@ -423,26 +427,31 @@ export function createBoolClient(config: BoolClientConfig): BoolClient {
},
};

// Realtime "doorbell": the app schema's grants are revoked, so Supabase
// `postgres_changes` never fires. Instead the server broadcasts a
// row-data-free ping on the PUBLIC channel "bool:" + schema whenever any
// row changes. Subscribe with the anon key (no token needed) and REFETCH
// on each ping.
const subscribeToChanges = (
listener: (payload: BoolChangePayload) => void,
): (() => void) => {
const channel = db
.channel("bool:" + schema)
.on("broadcast", { event: "*" }, (msg) =>
listener((msg as { payload?: BoolChangePayload }).payload ?? {}),
)
.subscribe();
return () => {
void db.removeChannel(channel);
};
};

const client: BoolClient = {
db,
entities: createEntitiesModule(db, subscribeToChanges),
auth,
schema,
// Realtime "doorbell": the app schema's grants are revoked, so Supabase
// `postgres_changes` never fires. Instead the server broadcasts a
// row-data-free ping on the PUBLIC channel "bool:" + schema whenever any
// row changes. Subscribe with the anon key (no token needed) and REFETCH
// on each ping.
subscribeToChanges(listener) {
const channel = db
.channel("bool:" + schema)
.on("broadcast", { event: "*" }, (msg) =>
listener((msg as { payload?: BoolChangePayload }).payload ?? {}),
)
.subscribe();
return () => {
void db.removeChannel(channel);
};
},
subscribeToChanges,
};

setDefaultBoolClient(client);
Expand Down
Loading
Loading