Chelabase
TypeScript client

TypeScript client

A typed client for the Chelabase data API, built on fetch with no runtime dependencies.

@chelabase/client is a typed client for the data API (POST /data/v1/query). It has no runtime dependencies: it's built on fetch, so it runs in browsers, Node and edge runtimes.

The package lives in the repository (packages/client) and is a workspace package, not yet published to npm. Add it to a workspace app as "@chelabase/client": "workspace:*".

Create a client

The generated types (db.ts, from Studio's Download types or GET /data/v1/types) hold a Database type and a types constant from a branch's schema (see Generated types). Pass both to createClient, with the gateway's URL and a key:

import {  } from "@chelabase/client";
import { type ,  } from "./db";

const  = <>({ : "http://api.chelabase.localhost:8000", : "chk_pub_…",  });

The key is sent as a bearer token. A public key (role anon) is safe in a browser; keep a secret key on a server.

Read

db.from(table) starts a query; "billing.invoices" is a qualified name, and a bare name means the database's default schema. Builders are immutable and lazy: nothing is sent until you await (each await sends the request once). They also have .catch() and .finally().

const  = await 
  .("orders")
  .("id", "total", { : "slug", : "handle" })
  .({ : "paid", : { : "100" }, : [{ : { : 2 } }, { : true }] })
  .("created_at", "desc", "last")
  .(50);

const first = [0];
const first: {
    handle: string | null;
    id: number;
    total: string;
}

select() and returning() need at least one column. Leave select() out for every column. Filters are covered on Filters.

count() counts the matching rows as your role sees them (row-level security applies) and resolves to { count: number }. It sends only the table and the filter; a count after limit() or offset() throws.

const {  } = await .("orders").({ : true }).();

toTree() returns the exact JSON tree a builder sends, the query language of the data API:

const tree = .("orders").("id").({ : true }).(1).();
const tree: QueryTree

Write

// Insert: all rows must have the same keys; returning is optional.
const created = await db
  .from("orders")
  .insert([{ total: "12.50", vip: false, history: [] }])
  .returning("id", "total");

// Update and delete: .where() or .all() first.
await db.from("orders").update({ status: "paid" }).where({ id: created[0].id });
await db.from("orders").delete().where({ vip: false }).returning("*");
await db.from("billing.invoices").delete().all();

An update or delete runs only after .where() or .all(). Before that, awaiting it or putting it in a batch is a compile error (and the server refuses it anyway). .returning() works before or after:

await db.from("orders").update({ status: "paid" });
Type of 'await' operand must either be a valid promise or must not contain a callable 'then' member.

Call a function

const points = await .("add_points", { : "7c9e6679-7425-40de-944b-e07fc1f90ae7", : 5 });
const points: number
const next = await .("next_id");
const next: number

Batch

db.batch runs its operations in order in one transaction, all or nothing (at most 50), and resolves to a typed tuple with each result. Batches don't nest:

const [, ] = await .(() => [
  .("orders").({ : "1.00", : false, : [] }).("id"),
  .("add_points", { : "7c9e6679-7425-40de-944b-e07fc1f90ae7" }),
]);
const results = { ,  };
const results: {
    order: Returned<ResolveRow<{
        amounts: string[] | null;
        and: string | null;
        created_at: string;
        history: ("paid" | "pending" | "ship\"ped")[];
        id: Int8;
        ids: Int8[];
        meta: Json;
        qty: number;
        ratio: number | null;
        raw: Json;
        seq: Int8 | null;
        "ship-to": string | null;
        slug: string | null;
        status: "paid" | "pending" | "ship\"ped";
        tags: string[] | null;
        total: string;
        vip: boolean;
    }, false>, readonly ["id"]>;
    points: number;
}

More

On this page