npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@colixsystems/datastore-client

v0.12.0

Published

Typed, scoped data-plane client for the AppStudio datastore API (tables, records, aggregates, record-level permissions, BankID e-signing, realtime subscribe). snake_case wire contract, no transform.

Readme

@colixsystems/datastore-client

Typed, scoped data-plane client for the AppStudio datastore API. It covers exactly three things:

  • tables — table schema (id, name, columns) via tables.{list,get} and the schema(tableId) alias.
  • records — record CRUD, querying, and aggregation via records(tableId).{list,get,create,update,delete,aggregate}.
  • record-level permissions — RLS grants on a single record (REQ-ACL-06) via records(tableId).permissions(recordId).{list,grant,update,revoke}.
  • realtime — subscribe to a table's live change stream (REQ-RT-07) via records(tableId).subscribe({ onCreated, onUpdated, onDeleted, onStatus }), returning an unsubscribe function.

It does not cover users, groups, files, or payments — those live in sibling packages (assets-client, directory-client, payments-client). This is a standalone fetch-based client you instantiate yourself with createDatastoreClient({ baseUrl, getToken, getTenantId }).

Two surfaces, one package. This client serves two callers:

  1. External / server-side integrations instantiate it directly with an API key (getToken returns the key, getTenantId the tenant) and call the methods below.
  2. The widget runtime — the Player and exported Expo app instantiate the same package and inject it into WidgetContext.datastore. Widgets never import this package; they call the SDK hooks from @colixsystems/widget-sdk (useDatastoreQuery, useDatastoreMutation, useDatastoreSchema, useRecordPermissions, …), which read ctx.datastore. Both surfaces speak the identical snake_case REST contract.

Status

v0.12.0 — pre-publish. Not yet published to npm.

0.12.0 (additive): records(tableId).list({ sort, filter }) and .aggregate({ filter }) now NORMALISE their sort and filter arguments, so the structured shapes authors (and the AI widget agent) reach for work alongside the raw wire shapes:

  • sort accepts the <col>:<dir> wire string (e.g. created_at:desc), a { field, dir }, or an array of those (the backend sorts on a single column — the first entry is used).
  • filter accepts the wire map ({ status: "eq:PAID" }) OR the structured condition array the filterList author control emits ([{ column, operator, value, valueMode }]); a relativeDate value mode resolves to an ISO timestamp, and empty/nempty drop their value.
  • An un-normalizable sort/filter now throws ValidationError instead of stringifying to [object Object] and producing a silent 400.
  • Doc fix: the descending-sort form is created_at:desc (NOT -created_at, which the backend reads as a column literally named -created_at).

0.10.0 (breaking): removed records(tableId).sign(recordId) and its SignInitiate*/SignStatusResult/RecordSignatureNamespace types. Per-record/column BankID signing (the retired SIGNATURE column type) is replaced by a standalone Signature subject model — signing a file first — exposed through new methods as that backend ships. No other method changed.

0.6.0 (additive): added records(tableId).subscribe({ onCreated, onUpdated, onDeleted, onStatus }, { fallbackAfterMs? }) (REQ-RT-07) — a WebSocket subscription to the table's realtime change stream at <baseUrl>/datastore/ws, returning a synchronous unsubscribe function. Server-gated by the same read ACL as REST. New optional webSocketImpl factory option (defaults to globalThis.WebSocket, present in browsers and React Native); when no impl is available onStatus reports "fallback" so callers poll. No existing method changed.

0.5.0 (breaking): the client is now snake_case end to end with NO transform (REQ-GEN-09) and is scoped to the data plane only.

  • The old camelCase↔snake_case permission mappers are deleted. Permission bodies are sent snake_case verbatim ({ user_id, can_read, … }) and rows are returned snake_case verbatim ({ id, table_id, can_read, … }).
  • records().list() returns the { data, meta } envelope verbatim (no unwrap). tables.list() likewise.
  • Added schema(tableId) as an alias of tables.get for the column structure (the host calls ctx.datastore.schema(t)).
  • Added an optional getRequestHeaders({ namespace, operation }) factory option for attaching per-request headers (e.g. per-widget scope tokens) without the SDK knowing about scope-token issuance.
  • record list now accepts a sort param (e.g. created_at:desc).
  • Removed the users / groups namespaces — they belong in sibling packages.

Contract: snake_case, no transform

The wire format is snake_case in both directions and that is the client contract too. The SDK does no case mapping:

  • Request bodies are sent snake_case verbatim — you pass { user_id, can_read }, not { userId, canRead }.
  • Response objects are returned snake_case verbatim — you read row.created_at, row.table_id, perm.can_read.
  • The only camelCase is JS method names (filterMode, groupBy, sumField) and factory option names.

Public API

import {
  createDatastoreClient,
  DatastoreError,
  NotFoundError,
  ForbiddenError,
  ValidationError,
  RateLimitedError,
  ServerError,
} from "@colixsystems/datastore-client";

const client = createDatastoreClient({
  baseUrl: "https://api.appstudio.io",
  getToken: () => "Bearer ...",        // "Bearer " prefix added if missing
  getTenantId: () => "tenant_abc",
  getRequestHeaders: ({ namespace, operation }) => ({ "X-Widget-Scopes": "..." }), // optional
  // fetchImpl defaults to globalThis.fetch
});

// Tables (schema)
const { data: tables, meta } = await client.tables.list();   // { data, meta } verbatim
const table = await client.tables.get("Tasks");              // { id, name, columns: [...] }
const sameTable = await client.schema("Tasks");              // alias of tables.get

// Records — filter values are `op:value` expressions (eq, neq, lt, gt, gte,
// lte, contains, empty, nempty). Sort is `<col>:<asc|desc>`. Pagination is
// limit + offset.
const { data: orders } = await client
  .records("orders")
  .list({ filter: { status: "eq:PAID" }, sort: "created_at:desc", limit: 50, offset: 0 });

// `sort` and `filter` also accept the structured shapes authors reach for —
// they normalise to the wire form above (an un-normalizable shape throws
// ValidationError rather than emitting `[object Object]`):
const { data: recent } = await client
  .records("orders")
  .list({
    sort: { field: "created_at", dir: "desc" },
    filter: [{ column: "status", operator: "eq", value: "PAID" }],
  });

const rec = await client.records("orders").get("r1");
await client.records("orders").create({ status: "PAID", amount_cents: 1200 });
await client.records("orders").update("r1", { status: "REFUNDED" }); // PATCH
await client.records("orders").delete("r1");

// Aggregate: group by one column, optionally sum one numeric field.
const byStatus = await client
  .records("orders")
  .aggregate({ groupBy: "status", sumField: "amount_cents" });
// → [{ group: "PAID", count: 12, sum: 4200 }, ...]

// Row-level permissions (REQ-ACL-06): a grant to a user OR group with
// can_read is what membership means. Provide exactly one of user_id/group_id.
const perms = client.records("channels").permissions(channelId);
const { data: members } = await perms.list();                // { data, meta } verbatim
const grant = await perms.grant({ user_id: "user_123", can_read: true });
await perms.update(grant.id, { can_write: true });
await perms.revoke(grant.id);

Surface

| Method | HTTP | Returns | | --- | --- | --- | | tables.list() | GET /tables | Page<Table> | | tables.get(idOrName) | GET /tables/{id} | Table (with columns) | | schema(tableId) | GET /tables/{id} | Table (alias of tables.get) | | records(t).list(query?) | GET /tables/{t}/records | Page<Record> | | records(t).get(id) | GET /tables/{t}/records/{id} | Record | | records(t).create(values) | POST /tables/{t}/records | Record | | records(t).update(id, values) | PATCH /tables/{t}/records/{id} | Record | | records(t).delete(id) | DELETE /tables/{t}/records/{id} | void | | records(t).aggregate(spec) | GET /tables/{t}/records/aggregate | AggregateResult | | records(t).permissions(r).list() | GET /tables/{t}/records/{r}/permissions | Page<RecordPermission> | | records(t).permissions(r).grant(body) | POST /tables/{t}/records/{r}/permissions | RecordPermission | | records(t).permissions(r).update(pid, patch) | PUT /tables/{t}/records/{r}/permissions/{pid} | RecordPermission | | records(t).permissions(r).revoke(pid) | DELETE /tables/{t}/records/{r}/permissions/{pid} | void | | records(t).subscribe(handlers, opts?) | WS <baseUrl>/datastore/ws ({type:"subscribe",tableId}) | () => void (unsubscribe) |

Query params (snake_case on the wire)

| Caller key | Wire param | | --- | --- | | limit | limit | | offset | offset | | q | q | | filterMode | filter_mode (and | or) | | sort | sort<col>:<asc\|desc> (e.g. created_at:desc); also accepts { field, dir } or an array | | filter[col] | filter[col]=op:value (inner key is the author's column name, verbatim); also accepts [{ column, operator, value, valueMode }] | | groupBy | group_by | | sumField | sum_field |

Factory options

| Option | Type | Notes | | --- | --- | --- | | baseUrl | string | Required. | | getToken | () => string \| Promise<string> | Required. Returns the Authorization value; Bearer prefix added if missing. | | getTenantId | () => string \| Promise<string> | Required. Returns the x-tenant-id value. | | getRequestHeaders | ({ namespace, operation }) => object \| Promise<object> | Optional. Extra headers merged per request (e.g. scope tokens). namespace is one of tables / records / permissions. | | fetchImpl | typeof fetch | Optional. Defaults to globalThis.fetch. | | webSocketImpl | typeof WebSocket | Optional. Used by records(t).subscribe(...). Defaults to globalThis.WebSocket (browser + React Native). When absent, subscribe reports "fallback" and opens no socket. |

Transport

| Concern | Behaviour | | --- | --- | | Auth header | From getToken (host-injected); Bearer prefix normalised. | | Tenant header | x-tenant-id from getTenantId (host-injected). | | Retries | Idempotent GETs retried 3× with exponential backoff (200/400/800 ms). | | Timeouts | 10 s default, configurable per call via timeoutMs. | | Error model | Typed DatastoreError hierarchy (NotFoundError, ForbiddenError, ValidationError, RateLimitedError, ServerError). | | Platform | Browser and React Native (uses fetch + AbortController). |

Dependencies

None. The client uses only platform fetch and AbortController, both available in modern browsers, Node 18+, and React Native.

Tests

node --test src

Fully self-contained — no npm install, no cross-package deps.