@colixsystems/datastore-client 0.2.0 → 0.3.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 +34 -2
- package/dist/client.js +70 -21
- package/dist/client.test.js +121 -0
- package/dist/index.d.ts +47 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,9 @@ See the design reference: [`docs/architecture/widget-marketplace.md`](../../docs
|
|
|
6
6
|
|
|
7
7
|
## Status
|
|
8
8
|
|
|
9
|
-
`v0.
|
|
9
|
+
`v0.3.0` — pre-publish. Not yet published to npm.
|
|
10
|
+
|
|
11
|
+
> **0.3.0 (breaking):** the client now matches the AppStudio backend's real REST contract. `records().list()` takes `{ limit, offset, q, filterMode, filter }` (the old `cursor` / JSON `filter` and `sort` are gone) and returns the `{ data, meta }` envelope. `records().aggregate({ groupBy, sumField, filter })` is a `GET` returning `[{ group, count, sum }]`. `users.me()` resolves the current principal via `/auth/app/me`. Filter values are `op:value` expressions.
|
|
10
12
|
|
|
11
13
|
## Differences from `frontend/src/api/client.js`
|
|
12
14
|
|
|
@@ -40,9 +42,39 @@ const client = createDatastoreClient({
|
|
|
40
42
|
});
|
|
41
43
|
|
|
42
44
|
const tables = await client.tables.list();
|
|
43
|
-
|
|
45
|
+
|
|
46
|
+
// Filter values are `op:value` expressions (eq, neq, lt, gt, gte, lte,
|
|
47
|
+
// contains, empty, nempty). Pagination is limit + offset.
|
|
48
|
+
const { data: orders, meta } = await client
|
|
49
|
+
.records("orders")
|
|
50
|
+
.list({ filter: { status: "eq:PAID" }, limit: 50, offset: 0 });
|
|
51
|
+
|
|
52
|
+
// Aggregate: group by one column, optionally sum one numeric field.
|
|
53
|
+
const byStatus = await client
|
|
54
|
+
.records("orders")
|
|
55
|
+
.aggregate({ groupBy: "status", sumField: "total" });
|
|
56
|
+
// → [{ group: "PAID", count: 12, sum: 4200 }, ...]
|
|
57
|
+
|
|
58
|
+
const me = await client.users.me(); // GET /auth/app/me
|
|
59
|
+
const myGroups = await client.groups.listMine(); // { data, meta }
|
|
44
60
|
```
|
|
45
61
|
|
|
62
|
+
## Surface
|
|
63
|
+
|
|
64
|
+
| Method | HTTP | Returns |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| `tables.list()` | `GET /tables` | `Page<TableMeta>` |
|
|
67
|
+
| `tables.get(idOrName)` | `GET /tables/{id}` | `TableMeta` |
|
|
68
|
+
| `records(t).list(query?)` | `GET /tables/{t}/records` | `Page<Record>` |
|
|
69
|
+
| `records(t).get(id)` | `GET /tables/{t}/records/{id}` | `Record` |
|
|
70
|
+
| `records(t).create(values)` | `POST /tables/{t}/records` | `Record` |
|
|
71
|
+
| `records(t).update(id, values)` | `PATCH /tables/{t}/records/{id}` | `Record` |
|
|
72
|
+
| `records(t).delete(id)` | `DELETE /tables/{t}/records/{id}` | `void` |
|
|
73
|
+
| `records(t).aggregate(spec)` | `GET /tables/{t}/records/aggregate` | `AggregateResult` |
|
|
74
|
+
| `users.me()` | `GET /auth/app/me` | `AppUser` |
|
|
75
|
+
| `users.get(id)` | `GET /app/users/{id}` | `AppUser` |
|
|
76
|
+
| `groups.listMine()` | `GET /app/groups/mine` | `Page<Group>` |
|
|
77
|
+
|
|
46
78
|
## Dependencies
|
|
47
79
|
|
|
48
80
|
None. The client uses only platform `fetch` and `AbortController`, both available in modern browsers, Node 18+, and React Native.
|
package/dist/client.js
CHANGED
|
@@ -12,20 +12,50 @@ function joinUrl(base, path) {
|
|
|
12
12
|
return `${b}${p}`;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// Serialise a column-filter map into the backend's bracket form
|
|
16
|
+
// (`filter[col]=op:value`). The column names are author-authored and
|
|
17
|
+
// pass through verbatim (the backend exempts the `filter` subtree from
|
|
18
|
+
// its snake_case query normalisation); the `op:value` expression is URL-
|
|
19
|
+
// encoded. Used by both the record list and the aggregate query.
|
|
20
|
+
function appendFilter(parts, filter) {
|
|
21
|
+
if (!filter || typeof filter !== "object") return;
|
|
22
|
+
for (const [col, expr] of Object.entries(filter)) {
|
|
23
|
+
if (expr == null) continue;
|
|
24
|
+
parts.push(
|
|
25
|
+
`filter[${encodeURIComponent(col)}]=${encodeURIComponent(expr)}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Record-list query string. Mirrors the backend contract
|
|
31
|
+
// (`GET /tables/:id/records`): `limit` + `offset` pagination, free-text
|
|
32
|
+
// `q`, `filter_mode` (and|or), and `filter[col]=op:value` conditions.
|
|
15
33
|
function buildQueryString(query) {
|
|
16
34
|
if (!query || typeof query !== "object") return "";
|
|
17
35
|
const parts = [];
|
|
18
|
-
if (query.limit != null)
|
|
19
|
-
|
|
20
|
-
if (query.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (query.filter && typeof query.filter === "object") {
|
|
27
|
-
parts.push(`filter=${encodeURIComponent(JSON.stringify(query.filter))}`);
|
|
36
|
+
if (query.limit != null)
|
|
37
|
+
parts.push(`limit=${encodeURIComponent(query.limit)}`);
|
|
38
|
+
if (query.offset != null)
|
|
39
|
+
parts.push(`offset=${encodeURIComponent(query.offset)}`);
|
|
40
|
+
if (query.q != null && query.q !== "")
|
|
41
|
+
parts.push(`q=${encodeURIComponent(query.q)}`);
|
|
42
|
+
if (query.filterMode === "or" || query.filterMode === "and") {
|
|
43
|
+
parts.push(`filter_mode=${query.filterMode}`);
|
|
28
44
|
}
|
|
45
|
+
appendFilter(parts, query.filter);
|
|
46
|
+
return parts.length ? `?${parts.join("&")}` : "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Aggregate query string. Mirrors the backend contract
|
|
50
|
+
// (`GET /tables/:id/records/aggregate`): a single `group_by` column, a
|
|
51
|
+
// single `sum_field`, and optional `filter[col]=op:value` conditions.
|
|
52
|
+
function buildAggregateQueryString(spec) {
|
|
53
|
+
if (!spec || typeof spec !== "object") return "";
|
|
54
|
+
const parts = [];
|
|
55
|
+
if (spec.groupBy) parts.push(`group_by=${encodeURIComponent(spec.groupBy)}`);
|
|
56
|
+
if (spec.sumField)
|
|
57
|
+
parts.push(`sum_field=${encodeURIComponent(spec.sumField)}`);
|
|
58
|
+
appendFilter(parts, spec.filter);
|
|
29
59
|
return parts.length ? `?${parts.join("&")}` : "";
|
|
30
60
|
}
|
|
31
61
|
|
|
@@ -48,12 +78,14 @@ export function createDatastoreClient(opts) {
|
|
|
48
78
|
throw new TypeError("createDatastoreClient: getToken must be a function");
|
|
49
79
|
}
|
|
50
80
|
if (typeof getTenantId !== "function") {
|
|
51
|
-
throw new TypeError(
|
|
81
|
+
throw new TypeError(
|
|
82
|
+
"createDatastoreClient: getTenantId must be a function",
|
|
83
|
+
);
|
|
52
84
|
}
|
|
53
85
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
54
86
|
if (typeof fetchImpl !== "function") {
|
|
55
87
|
throw new TypeError(
|
|
56
|
-
"createDatastoreClient: no fetch available. Pass fetchImpl or run in an environment with global fetch."
|
|
88
|
+
"createDatastoreClient: no fetch available. Pass fetchImpl or run in an environment with global fetch.",
|
|
57
89
|
);
|
|
58
90
|
}
|
|
59
91
|
|
|
@@ -63,7 +95,10 @@ export function createDatastoreClient(opts) {
|
|
|
63
95
|
const tenantId = await getTenantId();
|
|
64
96
|
|
|
65
97
|
const headers = { accept: "application/json" };
|
|
66
|
-
if (token)
|
|
98
|
+
if (token)
|
|
99
|
+
headers.authorization = token.startsWith("Bearer ")
|
|
100
|
+
? token
|
|
101
|
+
: `Bearer ${token}`;
|
|
67
102
|
if (tenantId) headers["x-tenant-id"] = tenantId;
|
|
68
103
|
if (body !== undefined) headers["content-type"] = "application/json";
|
|
69
104
|
|
|
@@ -106,27 +141,41 @@ export function createDatastoreClient(opts) {
|
|
|
106
141
|
}
|
|
107
142
|
const enc = encodeURIComponent(table);
|
|
108
143
|
return {
|
|
109
|
-
list: (query) =>
|
|
110
|
-
|
|
111
|
-
|
|
144
|
+
list: (query) =>
|
|
145
|
+
request("GET", `/tables/${enc}/records${buildQueryString(query)}`),
|
|
146
|
+
get: (id) =>
|
|
147
|
+
request("GET", `/tables/${enc}/records/${encodeURIComponent(id)}`),
|
|
148
|
+
create: (values) =>
|
|
149
|
+
request("POST", `/tables/${enc}/records`, { body: values }),
|
|
112
150
|
update: (id, values) =>
|
|
113
|
-
request("PATCH", `/tables/${enc}/records/${encodeURIComponent(id)}`, {
|
|
114
|
-
|
|
115
|
-
|
|
151
|
+
request("PATCH", `/tables/${enc}/records/${encodeURIComponent(id)}`, {
|
|
152
|
+
body: values,
|
|
153
|
+
}),
|
|
154
|
+
delete: (id) =>
|
|
155
|
+
request("DELETE", `/tables/${enc}/records/${encodeURIComponent(id)}`),
|
|
156
|
+
aggregate: (spec) =>
|
|
157
|
+
request(
|
|
158
|
+
"GET",
|
|
159
|
+
`/tables/${enc}/records/aggregate${buildAggregateQueryString(spec)}`,
|
|
160
|
+
),
|
|
116
161
|
};
|
|
117
162
|
}
|
|
118
163
|
|
|
119
164
|
return {
|
|
120
165
|
tables: {
|
|
121
166
|
list: () => request("GET", `/tables`),
|
|
122
|
-
get: (idOrName) =>
|
|
167
|
+
get: (idOrName) =>
|
|
168
|
+
request("GET", `/tables/${encodeURIComponent(idOrName)}`),
|
|
123
169
|
},
|
|
124
170
|
records: recordsNs,
|
|
125
171
|
users: {
|
|
126
|
-
|
|
172
|
+
// The current principal. For an app-user JWT this is the logged-in
|
|
173
|
+
// user; for an INTEGRATION API key it is the bound service account.
|
|
174
|
+
me: () => request("GET", `/auth/app/me`),
|
|
127
175
|
get: (id) => request("GET", `/app/users/${encodeURIComponent(id)}`),
|
|
128
176
|
},
|
|
129
177
|
groups: {
|
|
178
|
+
// The groups the calling principal belongs to.
|
|
130
179
|
listMine: () => request("GET", `/app/groups/mine`),
|
|
131
180
|
},
|
|
132
181
|
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Wire-contract tests for createDatastoreClient. A capturing fetch
|
|
2
|
+
// implementation records the method + URL each namespace method emits so
|
|
3
|
+
// we lock the client to the backend's real REST surface (REQ-OPS-DOC-EXT
|
|
4
|
+
// reconciliation). These run under `node --test`.
|
|
5
|
+
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { createDatastoreClient } from "./client.js";
|
|
9
|
+
|
|
10
|
+
function makeClient(captured, responseBody = { data: [], meta: {} }) {
|
|
11
|
+
const fetchImpl = async (url, init) => {
|
|
12
|
+
captured.url = url;
|
|
13
|
+
captured.method = init.method;
|
|
14
|
+
captured.body = init.body;
|
|
15
|
+
return {
|
|
16
|
+
ok: true,
|
|
17
|
+
status: 200,
|
|
18
|
+
text: async () => JSON.stringify(responseBody),
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
return createDatastoreClient({
|
|
22
|
+
baseUrl: "https://api.example.com/api/v1",
|
|
23
|
+
getToken: () => "Bearer as_testkey",
|
|
24
|
+
getTenantId: () => "tenant-1",
|
|
25
|
+
fetchImpl,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test("records.list serialises limit/offset/q/filter_mode/filter[col]", async () => {
|
|
30
|
+
const cap = {};
|
|
31
|
+
const client = makeClient(cap);
|
|
32
|
+
await client.records("Tasks").list({
|
|
33
|
+
limit: 25,
|
|
34
|
+
offset: 50,
|
|
35
|
+
q: "urgent",
|
|
36
|
+
filterMode: "or",
|
|
37
|
+
filter: { status: "eq:Open", priority: "gte:3" },
|
|
38
|
+
});
|
|
39
|
+
assert.equal(cap.method, "GET");
|
|
40
|
+
const u = new URL(cap.url);
|
|
41
|
+
assert.equal(u.pathname, "/api/v1/tables/Tasks/records");
|
|
42
|
+
assert.equal(u.searchParams.get("limit"), "25");
|
|
43
|
+
assert.equal(u.searchParams.get("offset"), "50");
|
|
44
|
+
assert.equal(u.searchParams.get("q"), "urgent");
|
|
45
|
+
assert.equal(u.searchParams.get("filter_mode"), "or");
|
|
46
|
+
assert.equal(u.searchParams.get("filter[status]"), "eq:Open");
|
|
47
|
+
assert.equal(u.searchParams.get("filter[priority]"), "gte:3");
|
|
48
|
+
// The legacy `cursor` / JSON `filter=` forms must be gone.
|
|
49
|
+
assert.equal(u.searchParams.get("cursor"), null);
|
|
50
|
+
assert.equal(u.searchParams.get("filter"), null);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("records.aggregate is a GET on /records/aggregate with group_by/sum_field", async () => {
|
|
54
|
+
const cap = {};
|
|
55
|
+
const client = makeClient(cap, []);
|
|
56
|
+
await client.records("Sales").aggregate({
|
|
57
|
+
groupBy: "region",
|
|
58
|
+
sumField: "amount",
|
|
59
|
+
filter: { status: "eq:Closed" },
|
|
60
|
+
});
|
|
61
|
+
assert.equal(cap.method, "GET");
|
|
62
|
+
assert.equal(cap.body, undefined);
|
|
63
|
+
const u = new URL(cap.url);
|
|
64
|
+
assert.equal(u.pathname, "/api/v1/tables/Sales/records/aggregate");
|
|
65
|
+
assert.equal(u.searchParams.get("group_by"), "region");
|
|
66
|
+
assert.equal(u.searchParams.get("sum_field"), "amount");
|
|
67
|
+
assert.equal(u.searchParams.get("filter[status]"), "eq:Closed");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("record CRUD verbs + paths", async () => {
|
|
71
|
+
const cap = {};
|
|
72
|
+
const client = makeClient(cap, { id: "r1" });
|
|
73
|
+
const ns = client.records("Tasks");
|
|
74
|
+
|
|
75
|
+
await ns.get("r1");
|
|
76
|
+
assert.equal(cap.method, "GET");
|
|
77
|
+
assert.equal(new URL(cap.url).pathname, "/api/v1/tables/Tasks/records/r1");
|
|
78
|
+
|
|
79
|
+
await ns.create({ title: "x" });
|
|
80
|
+
assert.equal(cap.method, "POST");
|
|
81
|
+
assert.equal(new URL(cap.url).pathname, "/api/v1/tables/Tasks/records");
|
|
82
|
+
|
|
83
|
+
await ns.update("r1", { title: "y" });
|
|
84
|
+
assert.equal(cap.method, "PATCH");
|
|
85
|
+
assert.equal(new URL(cap.url).pathname, "/api/v1/tables/Tasks/records/r1");
|
|
86
|
+
|
|
87
|
+
await ns.delete("r1");
|
|
88
|
+
assert.equal(cap.method, "DELETE");
|
|
89
|
+
assert.equal(new URL(cap.url).pathname, "/api/v1/tables/Tasks/records/r1");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("users.me targets /auth/app/me (not /app/users/me)", async () => {
|
|
93
|
+
const cap = {};
|
|
94
|
+
const client = makeClient(cap, { id: "u1", name: "A", email: "a@b.c" });
|
|
95
|
+
await client.users.me();
|
|
96
|
+
assert.equal(cap.method, "GET");
|
|
97
|
+
assert.equal(new URL(cap.url).pathname, "/api/v1/auth/app/me");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("users.get targets /app/users/{id}", async () => {
|
|
101
|
+
const cap = {};
|
|
102
|
+
const client = makeClient(cap, { id: "u9", name: "B" });
|
|
103
|
+
await client.users.get("u9");
|
|
104
|
+
assert.equal(new URL(cap.url).pathname, "/api/v1/app/users/u9");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("groups.listMine targets /app/groups/mine", async () => {
|
|
108
|
+
const cap = {};
|
|
109
|
+
const client = makeClient(cap);
|
|
110
|
+
await client.groups.listMine();
|
|
111
|
+
assert.equal(new URL(cap.url).pathname, "/api/v1/app/groups/mine");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("tables.list / tables.get paths", async () => {
|
|
115
|
+
const cap = {};
|
|
116
|
+
const client = makeClient(cap);
|
|
117
|
+
await client.tables.list();
|
|
118
|
+
assert.equal(new URL(cap.url).pathname, "/api/v1/tables");
|
|
119
|
+
await client.tables.get("Tasks");
|
|
120
|
+
assert.equal(new URL(cap.url).pathname, "/api/v1/tables/Tasks");
|
|
121
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -19,43 +19,65 @@ export interface Record_ {
|
|
|
19
19
|
}
|
|
20
20
|
export { Record_ as Record };
|
|
21
21
|
|
|
22
|
+
// The uniform list envelope every collection endpoint returns
|
|
23
|
+
// (`{ data, meta }`). `meta.total` is the ACL-aware row count; use it
|
|
24
|
+
// with `limit`/`offset` to page.
|
|
22
25
|
export interface Page<T> {
|
|
23
26
|
data: T[];
|
|
24
|
-
|
|
27
|
+
meta: {
|
|
28
|
+
total: number;
|
|
29
|
+
limit: number;
|
|
30
|
+
offset: number;
|
|
31
|
+
};
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
// Record-list query. A `filter` maps a column name to an `op:value`
|
|
35
|
+
// expression — e.g. `{ status: "eq:Open", priority: "gte:3" }`. Supported
|
|
36
|
+
// operators: eq, neq, lt, gt, gte, lte, contains, empty, nempty (and the
|
|
37
|
+
// `me` sentinel for USER columns). `filterMode` ANDs conditions by
|
|
38
|
+
// default, or ORs them. `q` is a free-text search across the table's
|
|
39
|
+
// string/text columns. Pagination is `limit` + `offset`.
|
|
27
40
|
export interface Query {
|
|
28
|
-
filter?: Record<string,
|
|
29
|
-
|
|
41
|
+
filter?: Record<string, string>;
|
|
42
|
+
filterMode?: "and" | "or";
|
|
43
|
+
q?: string;
|
|
30
44
|
limit?: number;
|
|
31
|
-
|
|
45
|
+
offset?: number;
|
|
32
46
|
}
|
|
33
47
|
|
|
48
|
+
// Aggregate request: group rows by a single column and (optionally) sum a
|
|
49
|
+
// single numeric field, with the same `filter` map the list accepts.
|
|
34
50
|
export interface AggregateSpec {
|
|
35
|
-
groupBy?: string
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
op: "count" | "sum" | "avg" | "min" | "max";
|
|
39
|
-
alias?: string;
|
|
40
|
-
}>;
|
|
41
|
-
filter?: Record<string, unknown>;
|
|
51
|
+
groupBy?: string;
|
|
52
|
+
sumField?: string;
|
|
53
|
+
filter?: Record<string, string>;
|
|
42
54
|
}
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
// Aggregate response: one row per group with the row count and the sum of
|
|
57
|
+
// `sumField` (0 when no `sumField` was requested).
|
|
58
|
+
export type AggregateResult = Array<{
|
|
59
|
+
group: string;
|
|
60
|
+
count: number;
|
|
61
|
+
sum: number;
|
|
62
|
+
}>;
|
|
47
63
|
|
|
64
|
+
// The principal returned by `users.me()` (`GET /auth/app/me`). `email` is
|
|
65
|
+
// present for the logged-in user; the directory projection from
|
|
66
|
+
// `users.get(id)` returns `{ id, name, role }` for non-Studio callers.
|
|
48
67
|
export interface AppUser {
|
|
49
68
|
id: string;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
groupIds
|
|
69
|
+
name: string | null;
|
|
70
|
+
email?: string | null;
|
|
71
|
+
role?: string;
|
|
72
|
+
groupIds?: string[];
|
|
54
73
|
}
|
|
55
74
|
|
|
56
75
|
export interface Group {
|
|
57
76
|
id: string;
|
|
58
77
|
name: string;
|
|
78
|
+
tenantId?: string;
|
|
79
|
+
createdAt?: string;
|
|
80
|
+
updatedAt?: string;
|
|
59
81
|
}
|
|
60
82
|
|
|
61
83
|
export interface RecordsNamespace {
|
|
@@ -90,7 +112,7 @@ export interface CreateDatastoreClientOptions {
|
|
|
90
112
|
}
|
|
91
113
|
|
|
92
114
|
export function createDatastoreClient(
|
|
93
|
-
opts: CreateDatastoreClientOptions
|
|
115
|
+
opts: CreateDatastoreClientOptions,
|
|
94
116
|
): DatastoreClient;
|
|
95
117
|
|
|
96
118
|
export class DatastoreError extends Error {
|
|
@@ -99,7 +121,7 @@ export class DatastoreError extends Error {
|
|
|
99
121
|
details: unknown;
|
|
100
122
|
constructor(
|
|
101
123
|
message: string,
|
|
102
|
-
init?: { code?: string; status?: number; details?: unknown }
|
|
124
|
+
init?: { code?: string; status?: number; details?: unknown },
|
|
103
125
|
);
|
|
104
126
|
}
|
|
105
127
|
export class NotFoundError extends DatastoreError {}
|
|
@@ -108,9 +130,12 @@ export class ValidationError extends DatastoreError {}
|
|
|
108
130
|
export class RateLimitedError extends DatastoreError {}
|
|
109
131
|
export class ServerError extends DatastoreError {}
|
|
110
132
|
|
|
111
|
-
export function errorFromResponse(
|
|
133
|
+
export function errorFromResponse(
|
|
134
|
+
status: number,
|
|
135
|
+
body: unknown,
|
|
136
|
+
): DatastoreError;
|
|
112
137
|
|
|
113
138
|
export function withRetry<T>(
|
|
114
139
|
opts: { method: string; timeoutMs?: number },
|
|
115
|
-
attempt: (signal: AbortSignal) => Promise<T
|
|
140
|
+
attempt: (signal: AbortSignal) => Promise<T>,
|
|
116
141
|
): Promise<T>;
|
package/package.json
CHANGED