@colixsystems/datastore-client 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -34
- package/dist/client.js +364 -93
- package/dist/client.test.js +298 -63
- package/dist/index.d.ts +133 -78
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,27 +1,39 @@
|
|
|
1
1
|
# @colixsystems/datastore-client
|
|
2
2
|
|
|
3
|
-
Typed, scoped client for the [AppStudio](https://github.com/appstudio) datastore API.
|
|
3
|
+
Typed, scoped **data-plane** client for the [AppStudio](https://github.com/appstudio) datastore API. It covers exactly three things:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- **tables** — table schema (id, name, columns) via `tables.{list,get}` and the `schema(tableId)` alias.
|
|
6
|
+
- **records** — record CRUD, querying, and aggregation via `records(tableId).{list,get,create,update,delete,aggregate}`.
|
|
7
|
+
- **record-level permissions** — RLS grants on a single record (REQ-ACL-06) via `records(tableId).permissions(recordId).{list,grant,update,revoke}`.
|
|
8
|
+
- **realtime** — subscribe to a table's live change stream (REQ-RT-07) via `records(tableId).subscribe({ onCreated, onUpdated, onDeleted, onStatus })`, returning an unsubscribe function.
|
|
9
|
+
|
|
10
|
+
It does **not** cover users, groups, files, or payments — those live in sibling packages ([`files-client`](../files-client), [`directory-client`](../directory-client), [`payments-client`](../payments-client)). This is a standalone `fetch`-based client you instantiate yourself with `createDatastoreClient({ baseUrl, getToken, getTenantId })`.
|
|
11
|
+
|
|
12
|
+
> **Two surfaces, one package.** This client serves two callers:
|
|
13
|
+
> 1. **External / server-side integrations** instantiate it directly with an API key (`getToken` returns the key, `getTenantId` the tenant) and call the methods below.
|
|
14
|
+
> 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.
|
|
6
15
|
|
|
7
16
|
## Status
|
|
8
17
|
|
|
9
|
-
`v0.
|
|
18
|
+
`v0.6.0` — pre-publish. Not yet published to npm.
|
|
10
19
|
|
|
11
|
-
> **0.
|
|
20
|
+
> **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.
|
|
12
21
|
|
|
13
|
-
> **0.
|
|
22
|
+
> **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**.
|
|
23
|
+
> - 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, … }`).
|
|
24
|
+
> - `records().list()` returns the `{ data, meta }` envelope verbatim (no unwrap). `tables.list()` likewise.
|
|
25
|
+
> - Added `schema(tableId)` as an alias of `tables.get` for the column structure (the host calls `ctx.datastore.schema(t)`).
|
|
26
|
+
> - 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.
|
|
27
|
+
> - `record list` now accepts a `sort` param (e.g. `-created_at`).
|
|
28
|
+
> - **Removed** the `users` / `groups` namespaces — they belong in sibling packages.
|
|
14
29
|
|
|
15
|
-
##
|
|
30
|
+
## Contract: snake_case, no transform
|
|
16
31
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
| Timeouts | Browser default | 10s default, configurable per call |
|
|
23
|
-
| Error model | Raw axios error | Typed `DatastoreError` hierarchy |
|
|
24
|
-
| Platform | Browser only | Browser **and** React Native (uses `fetch`) |
|
|
32
|
+
The wire format is snake_case in **both** directions and that is the client contract too. The SDK does **no** case mapping:
|
|
33
|
+
|
|
34
|
+
- Request bodies are sent **snake_case verbatim** — you pass `{ user_id, can_read }`, not `{ userId, canRead }`.
|
|
35
|
+
- Response objects are returned **snake_case verbatim** — you read `row.created_at`, `row.table_id`, `perm.can_read`.
|
|
36
|
+
- The only camelCase is JS method names (`filterMode`, `groupBy`, `sumField`) and factory option names.
|
|
25
37
|
|
|
26
38
|
## Public API
|
|
27
39
|
|
|
@@ -38,34 +50,40 @@ import {
|
|
|
38
50
|
|
|
39
51
|
const client = createDatastoreClient({
|
|
40
52
|
baseUrl: "https://api.appstudio.io",
|
|
41
|
-
getToken: () => "Bearer ...",
|
|
53
|
+
getToken: () => "Bearer ...", // "Bearer " prefix added if missing
|
|
42
54
|
getTenantId: () => "tenant_abc",
|
|
55
|
+
getRequestHeaders: ({ namespace, operation }) => ({ "X-Widget-Scopes": "..." }), // optional
|
|
43
56
|
// fetchImpl defaults to globalThis.fetch
|
|
44
57
|
});
|
|
45
58
|
|
|
46
|
-
|
|
59
|
+
// Tables (schema)
|
|
60
|
+
const { data: tables, meta } = await client.tables.list(); // { data, meta } verbatim
|
|
61
|
+
const table = await client.tables.get("Tasks"); // { id, name, columns: [...] }
|
|
62
|
+
const sameTable = await client.schema("Tasks"); // alias of tables.get
|
|
47
63
|
|
|
48
|
-
//
|
|
49
|
-
// contains, empty, nempty). Pagination is limit + offset.
|
|
50
|
-
const { data: orders
|
|
64
|
+
// Records — filter values are `op:value` expressions (eq, neq, lt, gt, gte,
|
|
65
|
+
// lte, contains, empty, nempty). Pagination is limit + offset.
|
|
66
|
+
const { data: orders } = await client
|
|
51
67
|
.records("orders")
|
|
52
|
-
.list({ filter: { status: "eq:PAID" }, limit: 50, offset: 0 });
|
|
68
|
+
.list({ filter: { status: "eq:PAID" }, sort: "-created_at", limit: 50, offset: 0 });
|
|
69
|
+
|
|
70
|
+
const rec = await client.records("orders").get("r1");
|
|
71
|
+
await client.records("orders").create({ status: "PAID", amount_cents: 1200 });
|
|
72
|
+
await client.records("orders").update("r1", { status: "REFUNDED" }); // PATCH
|
|
73
|
+
await client.records("orders").delete("r1");
|
|
53
74
|
|
|
54
75
|
// Aggregate: group by one column, optionally sum one numeric field.
|
|
55
76
|
const byStatus = await client
|
|
56
77
|
.records("orders")
|
|
57
|
-
.aggregate({ groupBy: "status", sumField: "
|
|
78
|
+
.aggregate({ groupBy: "status", sumField: "amount_cents" });
|
|
58
79
|
// → [{ group: "PAID", count: 12, sum: 4200 }, ...]
|
|
59
80
|
|
|
60
|
-
const me = await client.users.me(); // GET /auth/app/me
|
|
61
|
-
const myGroups = await client.groups.listMine(); // { data, meta }
|
|
62
|
-
|
|
63
81
|
// Row-level permissions (REQ-ACL-06): a grant to a user OR group with
|
|
64
|
-
//
|
|
82
|
+
// can_read is what membership means. Provide exactly one of user_id/group_id.
|
|
65
83
|
const perms = client.records("channels").permissions(channelId);
|
|
66
|
-
const { data: members } = await perms.list();
|
|
67
|
-
const grant = await perms.grant({
|
|
68
|
-
await perms.update(grant.id, {
|
|
84
|
+
const { data: members } = await perms.list(); // { data, meta } verbatim
|
|
85
|
+
const grant = await perms.grant({ user_id: "user_123", can_read: true });
|
|
86
|
+
await perms.update(grant.id, { can_write: true });
|
|
69
87
|
await perms.revoke(grant.id);
|
|
70
88
|
```
|
|
71
89
|
|
|
@@ -73,8 +91,9 @@ await perms.revoke(grant.id);
|
|
|
73
91
|
|
|
74
92
|
| Method | HTTP | Returns |
|
|
75
93
|
| --- | --- | --- |
|
|
76
|
-
| `tables.list()` | `GET /tables` | `Page<
|
|
77
|
-
| `tables.get(idOrName)` | `GET /tables/{id}` | `
|
|
94
|
+
| `tables.list()` | `GET /tables` | `Page<Table>` |
|
|
95
|
+
| `tables.get(idOrName)` | `GET /tables/{id}` | `Table` (with `columns`) |
|
|
96
|
+
| `schema(tableId)` | `GET /tables/{id}` | `Table` (alias of `tables.get`) |
|
|
78
97
|
| `records(t).list(query?)` | `GET /tables/{t}/records` | `Page<Record>` |
|
|
79
98
|
| `records(t).get(id)` | `GET /tables/{t}/records/{id}` | `Record` |
|
|
80
99
|
| `records(t).create(values)` | `POST /tables/{t}/records` | `Record` |
|
|
@@ -82,13 +101,54 @@ await perms.revoke(grant.id);
|
|
|
82
101
|
| `records(t).delete(id)` | `DELETE /tables/{t}/records/{id}` | `void` |
|
|
83
102
|
| `records(t).aggregate(spec)` | `GET /tables/{t}/records/aggregate` | `AggregateResult` |
|
|
84
103
|
| `records(t).permissions(r).list()` | `GET /tables/{t}/records/{r}/permissions` | `Page<RecordPermission>` |
|
|
85
|
-
| `records(t).permissions(r).grant(
|
|
104
|
+
| `records(t).permissions(r).grant(body)` | `POST /tables/{t}/records/{r}/permissions` | `RecordPermission` |
|
|
86
105
|
| `records(t).permissions(r).update(pid, patch)` | `PUT /tables/{t}/records/{r}/permissions/{pid}` | `RecordPermission` |
|
|
87
106
|
| `records(t).permissions(r).revoke(pid)` | `DELETE /tables/{t}/records/{r}/permissions/{pid}` | `void` |
|
|
88
|
-
| `
|
|
89
|
-
|
|
90
|
-
|
|
107
|
+
| `records(t).subscribe(handlers, opts?)` | `WS <baseUrl>/datastore/ws` (`{type:"subscribe",tableId}`) | `() => void` (unsubscribe) |
|
|
108
|
+
|
|
109
|
+
### Query params (snake_case on the wire)
|
|
110
|
+
|
|
111
|
+
| Caller key | Wire param |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `limit` | `limit` |
|
|
114
|
+
| `offset` | `offset` |
|
|
115
|
+
| `q` | `q` |
|
|
116
|
+
| `filterMode` | `filter_mode` (`and` \| `or`) |
|
|
117
|
+
| `sort` | `sort` (e.g. `-created_at`) |
|
|
118
|
+
| `filter[col]` | `filter[col]=op:value` (inner key is the author's column name, verbatim) |
|
|
119
|
+
| `groupBy` | `group_by` |
|
|
120
|
+
| `sumField` | `sum_field` |
|
|
121
|
+
|
|
122
|
+
## Factory options
|
|
123
|
+
|
|
124
|
+
| Option | Type | Notes |
|
|
125
|
+
| --- | --- | --- |
|
|
126
|
+
| `baseUrl` | `string` | Required. |
|
|
127
|
+
| `getToken` | `() => string \| Promise<string>` | Required. Returns the Authorization value; `Bearer ` prefix added if missing. |
|
|
128
|
+
| `getTenantId` | `() => string \| Promise<string>` | Required. Returns the `x-tenant-id` value. |
|
|
129
|
+
| `getRequestHeaders` | `({ namespace, operation }) => object \| Promise<object>` | Optional. Extra headers merged per request (e.g. scope tokens). `namespace` is one of `tables` / `records` / `permissions`. |
|
|
130
|
+
| `fetchImpl` | `typeof fetch` | Optional. Defaults to `globalThis.fetch`. |
|
|
131
|
+
| `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. |
|
|
132
|
+
|
|
133
|
+
## Transport
|
|
134
|
+
|
|
135
|
+
| Concern | Behaviour |
|
|
136
|
+
| --- | --- |
|
|
137
|
+
| Auth header | From `getToken` (host-injected); `Bearer ` prefix normalised. |
|
|
138
|
+
| Tenant header | `x-tenant-id` from `getTenantId` (host-injected). |
|
|
139
|
+
| Retries | Idempotent GETs retried 3× with exponential backoff (200/400/800 ms). |
|
|
140
|
+
| Timeouts | 10 s default, configurable per call via `timeoutMs`. |
|
|
141
|
+
| Error model | Typed `DatastoreError` hierarchy (`NotFoundError`, `ForbiddenError`, `ValidationError`, `RateLimitedError`, `ServerError`). |
|
|
142
|
+
| Platform | Browser **and** React Native (uses `fetch` + `AbortController`). |
|
|
91
143
|
|
|
92
144
|
## Dependencies
|
|
93
145
|
|
|
94
146
|
None. The client uses only platform `fetch` and `AbortController`, both available in modern browsers, Node 18+, and React Native.
|
|
147
|
+
|
|
148
|
+
## Tests
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
node --test src
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Fully self-contained — no `npm install`, no cross-package deps.
|