@atscript/db-client 0.1.43 → 0.1.44

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 CHANGED
@@ -5,7 +5,7 @@
5
5
  <h1 align="center">@atscript/db-client</h1>
6
6
 
7
7
  <p align="center">
8
- Browser-compatible HTTP client for <code>@atscript/moost-db</code> REST endpoints.
8
+ HTTP client for <code>@atscript/moost-db</code> REST endpoints.
9
9
  </p>
10
10
 
11
11
  <p align="center">
@@ -14,7 +14,9 @@
14
14
 
15
15
  ---
16
16
 
17
- Type-safe HTTP client that mirrors the server-side `AtscriptDbTable` API over REST. Works in browsers, Node.js, and any runtime with `fetch`. Supports the full query surface — filters, sorting, pagination, relation loading, text search, and aggregation.
17
+ Type-safe HTTP client that mirrors moost-db controller endpoints. Works in browsers, Node.js, and any runtime with `fetch`. Each method maps 1:1 to a controller endpoint — filters, sorting, pagination, relation loading, text search, and aggregation are all supported through typed query controls.
18
+
19
+ In SSR environments, Moost's `fetch` automatically routes local requests to handlers in-process, so the same `Client` instance works on both server and browser.
18
20
 
19
21
  ## Installation
20
22
 
@@ -32,27 +34,39 @@ const users = new Client<typeof User>("/api/users", {
32
34
  baseUrl: "https://api.example.com",
33
35
  });
34
36
 
35
- // Query
36
- const active = await users.findMany({ filter: { status: "active" } });
37
- const user = await users.findById("abc-123");
37
+ // Query → GET /query
38
+ const active = await users.query({ filter: { status: "active" } });
39
+
40
+ // Get one → GET /one/:id
41
+ const user = await users.one("abc-123");
42
+
43
+ // Insert → POST /
44
+ const { insertedId } = await users.insert({ name: "Alice" });
45
+
46
+ // Update → PATCH /
47
+ await users.update({ id: insertedId, role: "admin" });
48
+
49
+ // Remove → DELETE /:id
50
+ await users.remove(insertedId);
51
+
52
+ // Count → GET /query ($count)
53
+ const total = await users.count();
38
54
 
39
- // Write
40
- const { insertedId } = await users.insertOne({ name: "Alice" });
41
- await users.updateOne({ id: insertedId, role: "admin" });
42
- await users.deleteOne(insertedId);
55
+ // Paginate → GET /pages
56
+ const page = await users.pages({ filter: { active: true } }, 1, 20);
43
57
 
44
- // Metadata
58
+ // Metadata → GET /meta
45
59
  const meta = await users.meta();
46
60
  ```
47
61
 
48
62
  ## Features
49
63
 
50
- - **Full CRUD** — `findMany`, `findOne`, `findById`, `count`, `pages`, `insertOne`, `insertMany`, `updateOne`, `bulkUpdate`, `replaceOne`, `bulkReplace`, `deleteOne`
51
- - **URL query syntax** — filtering, sorting, pagination, field selection, relation loading via `$with`
52
- - **Search** — full-text (`search()`) and vector search via query controls
53
- - **Aggregation** — `$groupBy` with aggregate functions
64
+ - **Typed queries** — filter keys, sort fields, `$with` relation names, and primary keys are type-checked against the Atscript model
65
+ - **Full CRUD** — `query`, `count`, `pages`, `one`, `insert`, `update`, `replace`, `remove`
66
+ - **Aggregation** — typed `$groupBy` dimensions and measures with inferred result types
67
+ - **Search** — full-text and vector search via query controls
68
+ - **Client-side validation** — validates writes against the Atscript schema before sending
54
69
  - **Error handling** — `ClientError` with structured validation errors
55
- - **SSR isomorphism** — `DbInterface<T>` shared between server `AtscriptDbTable` and client `Client`
56
70
  - **Configurable** — custom `fetch`, static or async headers, base URL
57
71
 
58
72
  ## Documentation
package/dist/index.cjs CHANGED
@@ -20,15 +20,24 @@ var ClientError = class extends Error {
20
20
  //#endregion
21
21
  //#region src/client.ts
22
22
  /**
23
- * Browser-compatible HTTP client for moost-db REST endpoints.
23
+ * HTTP client for moost-db REST endpoints.
24
24
  *
25
- * Two usage modes (same class, different generic):
26
- * ```typescript
27
- * // Untyped broad Record<string, unknown> typing
28
- * const users = new Client('/db/tables/users')
25
+ * Each method maps 1:1 to a controller endpoint:
26
+ * - `query()` → `GET /query`
27
+ * - `count()` `GET /query` with `$count`
28
+ * - `aggregate()` `GET /query` with `$groupBy`
29
+ * - `pages()` → `GET /pages`
30
+ * - `one()` → `GET /one/:id` or `GET /one?compositeKeys`
31
+ * - `insert()` → `POST /`
32
+ * - `update()` → `PATCH /`
33
+ * - `replace()` → `PUT /`
34
+ * - `remove()` → `DELETE /:id` or `DELETE /?compositeKeys`
35
+ * - `meta()` → `GET /meta`
29
36
  *
30
- * // Type-safe — Atscript type as generic parameter
31
- * const users = new Client<typeof User>('/db/tables/users')
37
+ * ```typescript
38
+ * const users = new Client<typeof User>('/api/users')
39
+ * const all = await users.query()
40
+ * const page = await users.pages({ filter: { active: true } }, 1, 20)
32
41
  * ```
33
42
  */
34
43
  var Client = class {
@@ -37,124 +46,127 @@ var Client = class {
37
46
  _fetch;
38
47
  _headers;
39
48
  _metaPromise;
49
+ _validatorPromise;
40
50
  constructor(path, opts) {
41
51
  this._path = path.endsWith("/") ? path.slice(0, -1) : path;
42
52
  this._baseUrl = opts?.baseUrl ?? "";
43
53
  this._fetch = opts?.fetch ?? globalThis.fetch.bind(globalThis);
44
54
  this._headers = opts?.headers;
45
55
  }
46
- async findOne(query) {
47
- const controls = {
48
- ...query?.controls,
49
- $limit: 1
50
- };
51
- return (await this._get("query", {
52
- ...query,
53
- controls
54
- }))[0] ?? null;
55
- }
56
- async findMany(query) {
56
+ /**
57
+ * `GET /query` — query records with typed filter, sort, select, and relations.
58
+ */
59
+ async query(query) {
57
60
  return this._get("query", query);
58
61
  }
59
- async findById(id, query) {
60
- if (id !== null && typeof id === "object") {
61
- const params = this._idToParams(id);
62
- if (query?.controls) {
63
- const controlStr = (0, _uniqu_url_builder.buildUrl)({ controls: query.controls });
64
- if (controlStr) for (const [k, v] of new URLSearchParams(controlStr)) params.set(k, v);
65
- }
66
- const qs = params.toString();
67
- try {
68
- return await this._request("GET", `one${qs ? `?${qs}` : ""}`);
69
- } catch (e) {
70
- if (e instanceof ClientError && e.status === 404) return null;
71
- throw e;
72
- }
73
- }
74
- const controlStr = query?.controls ? (0, _uniqu_url_builder.buildUrl)({ controls: query.controls }) : "";
75
- try {
76
- return await this._request("GET", `one/${encodeURIComponent(String(id))}${controlStr ? `?${controlStr}` : ""}`);
77
- } catch (e) {
78
- if (e instanceof ClientError && e.status === 404) return null;
79
- throw e;
80
- }
81
- }
62
+ /**
63
+ * `GET /query` with `$count: true` returns record count.
64
+ */
82
65
  async count(query) {
83
- const controls = {
84
- ...query?.controls,
85
- $count: true
86
- };
87
66
  return this._get("query", {
88
67
  ...query,
89
- controls
68
+ controls: { $count: true }
90
69
  });
91
70
  }
92
- async findManyWithCount(query) {
93
- const controls = query?.controls ?? {};
94
- const limit = controls.$limit || 1e3;
95
- const skip = controls.$skip || 0;
96
- const page = Math.floor(skip / limit) + 1;
97
- const result = await this._get("pages", {
71
+ /**
72
+ * `GET /query` with `$groupBy` — aggregate query with typed dimension/measure fields.
73
+ */
74
+ async aggregate(query) {
75
+ return this._get("query", query);
76
+ }
77
+ /**
78
+ * `GET /pages` — paginated query with typed filter and relations.
79
+ */
80
+ async pages(query, page = 1, size = 10) {
81
+ return this._get("pages", {
98
82
  ...query,
99
83
  controls: {
100
- ...controls,
84
+ ...query?.controls,
101
85
  $page: page,
102
- $size: limit
86
+ $size: size
103
87
  }
104
88
  });
105
- return {
106
- data: result.data,
107
- count: result.count
108
- };
109
- }
110
- async pages(query) {
111
- return this._get("pages", query);
112
- }
113
- async search(text, query, indexName) {
114
- const controls = {
115
- ...query?.controls,
116
- $search: text
117
- };
118
- if (indexName) controls.$index = indexName;
119
- return this._get("query", {
120
- ...query,
121
- controls
122
- });
123
89
  }
124
- async aggregate(query) {
125
- return this._get("query", query);
126
- }
127
- async insertOne(data) {
128
- return this._request("POST", "", data);
90
+ /**
91
+ * `GET /one/:id` or `GET /one?k1=v1&k2=v2` — single record by primary key.
92
+ *
93
+ * Returns `null` on 404.
94
+ */
95
+ async one(id, query) {
96
+ const controlStr = query?.controls ? (0, _uniqu_url_builder.buildUrl)({ controls: query.controls }) : "";
97
+ if (id !== null && typeof id === "object") {
98
+ const params = this._idToParams(id);
99
+ if (controlStr) for (const [k, v] of new URLSearchParams(controlStr)) params.set(k, v);
100
+ const qs = params.toString();
101
+ return this._getOrNull(`one${qs ? `?${qs}` : ""}`);
102
+ }
103
+ return this._getOrNull(`one/${encodeURIComponent(String(id))}${controlStr ? `?${controlStr}` : ""}`);
129
104
  }
130
- async insertMany(data) {
105
+ async insert(data) {
106
+ await this._validateData(data, "insert");
131
107
  return this._request("POST", "", data);
132
108
  }
133
- async updateOne(data) {
134
- return this._request("PATCH", "", data);
135
- }
136
- async bulkUpdate(data) {
109
+ /**
110
+ * `PATCH /` — partial update one or many records by primary key.
111
+ */
112
+ async update(data) {
113
+ await this._validateData(data, "patch");
137
114
  return this._request("PATCH", "", data);
138
115
  }
139
- async replaceOne(data) {
116
+ /**
117
+ * `PUT /` — full replace one or many records by primary key.
118
+ */
119
+ async replace(data) {
120
+ await this._validateData(data, "replace");
140
121
  return this._request("PUT", "", data);
141
122
  }
142
- async bulkReplace(data) {
143
- return this._request("PUT", "", data);
144
- }
145
- async deleteOne(id) {
123
+ /**
124
+ * `DELETE /:id` or `DELETE /?k1=v1&k2=v2` — remove a record by primary key.
125
+ */
126
+ async remove(id) {
146
127
  if (id !== null && typeof id === "object") return this._request("DELETE", `?${this._idToParams(id).toString()}`);
147
128
  return this._request("DELETE", encodeURIComponent(String(id)));
148
129
  }
130
+ /**
131
+ * `GET /meta` — table/view metadata (cached after first call).
132
+ */
149
133
  async meta() {
150
- if (!this._metaPromise) this._metaPromise = this._request("GET", "meta");
134
+ if (!this._metaPromise) this._metaPromise = this._request("GET", "meta").catch((err) => {
135
+ this._metaPromise = void 0;
136
+ throw err;
137
+ });
151
138
  return this._metaPromise;
152
139
  }
140
+ /**
141
+ * Returns a lazily-initialized {@link ClientValidator} backed by the `/meta` type.
142
+ * Useful for accessing `flatMap` and `navFields` (e.g. for form generation).
143
+ */
144
+ getValidator() {
145
+ return this._getValidator();
146
+ }
147
+ async _validateData(data, mode) {
148
+ (await this._getValidator()).validate(data, mode);
149
+ }
150
+ _getValidator() {
151
+ if (!this._validatorPromise) this._validatorPromise = Promise.all([this.meta(), Promise.resolve().then(() => require("./validator.cjs"))]).then(([m, { createClientValidator }]) => createClientValidator(m)).catch((err) => {
152
+ this._validatorPromise = void 0;
153
+ throw err;
154
+ });
155
+ return this._validatorPromise;
156
+ }
153
157
  _idToParams(id) {
154
158
  const params = new URLSearchParams();
155
159
  for (const [k, v] of Object.entries(id)) params.set(k, String(v));
156
160
  return params;
157
161
  }
162
+ async _getOrNull(endpoint) {
163
+ try {
164
+ return await this._request("GET", endpoint);
165
+ } catch (e) {
166
+ if (e instanceof ClientError && e.status === 404) return null;
167
+ throw e;
168
+ }
169
+ }
158
170
  async _get(endpoint, query) {
159
171
  const qs = query ? (0, _uniqu_url_builder.buildUrl)(query) : "";
160
172
  return this._request("GET", `${endpoint}${qs ? `?${qs}` : ""}`);