@atscript/db-client 0.1.54 → 0.1.56

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/dist/index.cjs CHANGED
@@ -17,6 +17,28 @@ var ClientError = class extends Error {
17
17
  return this.body.errors ?? [];
18
18
  }
19
19
  };
20
+ /** Thrown by `Client.action()` when the action name is not present in `/meta`. */
21
+ var ActionNotFoundError = class extends Error {
22
+ name = "ActionNotFoundError";
23
+ constructor(action) {
24
+ super(`Action "${action}" is not declared on this controller`);
25
+ this.action = action;
26
+ }
27
+ };
28
+ /**
29
+ * Thrown by `Client.action()` for actions that cannot be invoked through
30
+ * the client — currently `processor: 'custom'` (UI-dispatched events,
31
+ * which the application is responsible for handling) and `processor: 'navigate'`
32
+ * when no browser environment and no `navigate` option are configured.
33
+ */
34
+ var ActionUnsupportedError = class extends Error {
35
+ name = "ActionUnsupportedError";
36
+ constructor(action, processor, message) {
37
+ super(message);
38
+ this.action = action;
39
+ this.processor = processor;
40
+ }
41
+ };
20
42
  //#endregion
21
43
  //#region src/client.ts
22
44
  /**
@@ -45,6 +67,7 @@ var Client = class {
45
67
  _baseUrl;
46
68
  _fetch;
47
69
  _headers;
70
+ _navigate;
48
71
  _metaPromise;
49
72
  _validatorPromise;
50
73
  constructor(path, opts) {
@@ -52,6 +75,7 @@ var Client = class {
52
75
  this._baseUrl = opts?.baseUrl ?? "";
53
76
  this._fetch = opts?.fetch ?? globalThis.fetch.bind(globalThis);
54
77
  this._headers = opts?.headers;
78
+ this._navigate = opts?.navigate;
55
79
  }
56
80
  /**
57
81
  * `GET /query` — query records with typed filter, sort, select, and relations.
@@ -138,6 +162,39 @@ var Client = class {
138
162
  return this._metaPromise;
139
163
  }
140
164
  /**
165
+ * Invoke a declared action by name. Resolves the action descriptor from the
166
+ * cached `/meta` response, then dispatches based on `processor`:
167
+ *
168
+ * - `'backend'` → POST `pk` (or `pks`) as a JSON body to the action's path
169
+ * and return the parsed server response. The HTTP method is always POST.
170
+ * - `'navigate'` → for `level: 'row'`, substitute `$1` in `value` with the
171
+ * PK (URL-encoded; composite PKs are URL-encoded per field and joined
172
+ * with `/`); for `level: 'rows'` or `'table'`, navigate to `value`
173
+ * verbatim. The default navigator (browser only) calls
174
+ * `window.location.assign(url)`. Provide `ClientOptions.navigate` to
175
+ * integrate with a SPA router.
176
+ * - `'custom'` → throw {@link ActionUnsupportedError}; UI-dispatched events
177
+ * are the application's responsibility, not the client's.
178
+ *
179
+ * Throws {@link ActionNotFoundError} when the action is not present in `/meta`.
180
+ *
181
+ * For `level: 'rows'`, `pk` must be an array. If a non-array is supplied
182
+ * for a `'rows'` action it is wrapped into a single-element array — the
183
+ * server-side `@DbActionPKs()` resolver requires an array body.
184
+ */
185
+ async action(name, pk) {
186
+ const action = (await this.meta()).actions.find((a) => a.name === name);
187
+ if (!action) throw new ActionNotFoundError(name);
188
+ if (action.processor === "custom") throw new ActionUnsupportedError(name, "custom", `Action "${name}" has processor "custom" — applications must dispatch custom actions themselves; the client cannot.`);
189
+ if (action.processor === "navigate") {
190
+ const url = this._interpolateNavigateUrl(action, pk);
191
+ await this._dispatchNavigate(action, url);
192
+ return;
193
+ }
194
+ const body = this._buildActionBody(action, pk);
195
+ return this._postAction(action, body);
196
+ }
197
+ /**
141
198
  * Returns a lazily-initialized {@link ClientValidator} backed by the `/meta` type.
142
199
  * Useful for accessing `flatMap` and `navFields` (e.g. for form generation).
143
200
  */
@@ -154,6 +211,34 @@ var Client = class {
154
211
  });
155
212
  return this._validatorPromise;
156
213
  }
214
+ _buildActionBody(action, pk) {
215
+ if (action.level === "table") return void 0;
216
+ if (action.level !== "rows") return pk;
217
+ if (Array.isArray(pk)) return pk;
218
+ return pk === void 0 ? [] : [pk];
219
+ }
220
+ _interpolateNavigateUrl(action, pk) {
221
+ if (action.level !== "row") return action.value;
222
+ if (pk === void 0) return action.value;
223
+ return action.value.replace(/\$1/g, encodeNavigatePk(pk));
224
+ }
225
+ async _dispatchNavigate(action, url) {
226
+ if (this._navigate) {
227
+ await this._navigate(url);
228
+ return;
229
+ }
230
+ const loc = globalThis.location;
231
+ if (loc?.assign) {
232
+ loc.assign(url);
233
+ return;
234
+ }
235
+ throw new ActionUnsupportedError(action.name, "navigate", `Action "${action.name}" is processor: 'navigate' but no browser is available and no \`navigate\` option was provided to Client.`);
236
+ }
237
+ async _postAction(action, body) {
238
+ const url = `${this._baseUrl}${action.value}`;
239
+ const init = await this._buildInit("POST", body);
240
+ return this._send(url, init, true);
241
+ }
157
242
  _idToParams(id) {
158
243
  const params = new URLSearchParams();
159
244
  for (const [k, v] of Object.entries(id)) params.set(k, String(v));
@@ -179,6 +264,10 @@ var Client = class {
179
264
  async _request(method, endpoint, body) {
180
265
  const sep = endpoint && !endpoint.startsWith("?") ? "/" : "";
181
266
  const url = `${this._baseUrl}${this._path}${sep}${endpoint}`;
267
+ const init = await this._buildInit(method, body);
268
+ return this._send(url, init, false);
269
+ }
270
+ async _buildInit(method, body) {
182
271
  const headers = { ...await this._resolveHeaders() };
183
272
  const init = {
184
273
  method,
@@ -188,6 +277,9 @@ var Client = class {
188
277
  headers["Content-Type"] = "application/json";
189
278
  init.body = JSON.stringify(body);
190
279
  }
280
+ return init;
281
+ }
282
+ async _send(url, init, allowEmpty) {
191
283
  const res = await this._fetch(url, init);
192
284
  if (!res.ok) {
193
285
  let errorBody;
@@ -201,9 +293,27 @@ var Client = class {
201
293
  }
202
294
  throw new ClientError(res.status, errorBody);
203
295
  }
204
- return res.json();
296
+ if (!allowEmpty) return res.json();
297
+ if (res.status === 204 || res.headers.get("content-length") === "0") return void 0;
298
+ try {
299
+ return await res.json();
300
+ } catch {
301
+ return;
302
+ }
205
303
  }
206
304
  };
305
+ /**
306
+ * Encode a row PK for substitution into a `processor: 'navigate'` URL template.
307
+ * Scalars are URL-encoded directly; composite PK objects have each value
308
+ * URL-encoded and joined with `/` in object-key order (which mirrors
309
+ * `primaryKeys` for the table).
310
+ */
311
+ function encodeNavigatePk(pk) {
312
+ if (pk === null || pk === void 0) return "";
313
+ return (typeof pk === "object" ? Object.values(pk) : [pk]).map((v) => encodeURIComponent(String(v))).join("/");
314
+ }
207
315
  //#endregion
316
+ exports.ActionNotFoundError = ActionNotFoundError;
317
+ exports.ActionUnsupportedError = ActionUnsupportedError;
208
318
  exports.Client = Client;
209
319
  exports.ClientError = ClientError;
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { C as TDbUpdateResult, E as UniqueryControls, S as TDbInsertResult, T as Uniquery, _ as RelationInfo, b as TDbDeleteResult, c as ClientOptions, d as FilterExpr, f as IdOf, g as PageResult, h as OwnOf, i as ValidatorMode, l as DataOf, m as NavOf, n as ClientValidator, o as AggregateQuery, p as MetaResponse, s as AggregateResult, t as ClientValidationError, u as FieldMeta, v as SearchIndexInfo, w as TypedWithRelation, x as TDbInsertManyResult, y as ServerError } from "./validator-DfU6oJir.cjs";
1
+ import { C as TDbUpdateResult, E as UniqueryControls, S as TDbInsertResult, T as Uniquery, _ as RelationInfo, b as TDbDeleteResult, c as ClientOptions, d as FilterExpr, f as IdOf, g as PageResult, h as OwnOf, i as ValidatorMode, l as DataOf, m as NavOf, n as ClientValidator, o as AggregateQuery, p as MetaResponse, s as AggregateResult, t as ClientValidationError, u as FieldMeta, v as SearchIndexInfo, w as TypedWithRelation, x as TDbInsertManyResult, y as ServerError } from "./validator-DNm9kCoq.cjs";
2
2
  import { AggregateQuery as AggregateQuery$1, AggregateResult as AggregateResult$1, Uniquery as Uniquery$1, UniqueryControls as UniqueryControls$1 } from "@uniqu/core";
3
- import { TDbDeleteResult as TDbDeleteResult$1, TDbInsertManyResult as TDbInsertManyResult$1, TDbInsertResult as TDbInsertResult$1, TDbUpdateResult as TDbUpdateResult$1 } from "@atscript/db";
3
+ import { TDbActionInfo, TDbActionIntent, TDbActionLevel, TDbActionProcessor, TDbDeleteResult as TDbDeleteResult$1, TDbInsertManyResult as TDbInsertManyResult$1, TDbInsertResult as TDbInsertResult$1, TDbUpdateResult as TDbUpdateResult$1 } from "@atscript/db";
4
4
  import { TSerializedAnnotatedType } from "@atscript/typescript/utils";
5
5
 
6
6
  //#region src/client.d.ts
@@ -34,6 +34,7 @@ declare class Client<T = Record<string, unknown>> {
34
34
  private readonly _baseUrl;
35
35
  private readonly _fetch;
36
36
  private readonly _headers?;
37
+ private readonly _navigate?;
37
38
  private _metaPromise?;
38
39
  private _validatorPromise?;
39
40
  constructor(path: string, opts?: ClientOptions);
@@ -90,6 +91,28 @@ declare class Client<T = Record<string, unknown>> {
90
91
  * `GET /meta` — table/view metadata (cached after first call).
91
92
  */
92
93
  meta(): Promise<MetaResponse>;
94
+ /**
95
+ * Invoke a declared action by name. Resolves the action descriptor from the
96
+ * cached `/meta` response, then dispatches based on `processor`:
97
+ *
98
+ * - `'backend'` → POST `pk` (or `pks`) as a JSON body to the action's path
99
+ * and return the parsed server response. The HTTP method is always POST.
100
+ * - `'navigate'` → for `level: 'row'`, substitute `$1` in `value` with the
101
+ * PK (URL-encoded; composite PKs are URL-encoded per field and joined
102
+ * with `/`); for `level: 'rows'` or `'table'`, navigate to `value`
103
+ * verbatim. The default navigator (browser only) calls
104
+ * `window.location.assign(url)`. Provide `ClientOptions.navigate` to
105
+ * integrate with a SPA router.
106
+ * - `'custom'` → throw {@link ActionUnsupportedError}; UI-dispatched events
107
+ * are the application's responsibility, not the client's.
108
+ *
109
+ * Throws {@link ActionNotFoundError} when the action is not present in `/meta`.
110
+ *
111
+ * For `level: 'rows'`, `pk` must be an array. If a non-array is supplied
112
+ * for a `'rows'` action it is wrapped into a single-element array — the
113
+ * server-side `@DbActionPKs()` resolver requires an array body.
114
+ */
115
+ action(name: string, pk?: unknown): Promise<unknown>;
93
116
  /**
94
117
  * Returns a lazily-initialized {@link ClientValidator} backed by the `/meta` type.
95
118
  * Useful for accessing `flatMap` and `navFields` (e.g. for form generation).
@@ -97,11 +120,17 @@ declare class Client<T = Record<string, unknown>> {
97
120
  getValidator(): Promise<ClientValidator>;
98
121
  private _validateData;
99
122
  private _getValidator;
123
+ private _buildActionBody;
124
+ private _interpolateNavigateUrl;
125
+ private _dispatchNavigate;
126
+ private _postAction;
100
127
  private _idToParams;
101
128
  private _getOrNull;
102
129
  private _get;
103
130
  private _resolveHeaders;
104
131
  private _request;
132
+ private _buildInit;
133
+ private _send;
105
134
  }
106
135
  //#endregion
107
136
  //#region src/client-error.d.ts
@@ -127,5 +156,23 @@ declare class ClientError extends Error {
127
156
  details?: unknown[];
128
157
  }[];
129
158
  }
159
+ /** Thrown by `Client.action()` when the action name is not present in `/meta`. */
160
+ declare class ActionNotFoundError extends Error {
161
+ readonly action: string;
162
+ name: string;
163
+ constructor(action: string);
164
+ }
165
+ /**
166
+ * Thrown by `Client.action()` for actions that cannot be invoked through
167
+ * the client — currently `processor: 'custom'` (UI-dispatched events,
168
+ * which the application is responsible for handling) and `processor: 'navigate'`
169
+ * when no browser environment and no `navigate` option are configured.
170
+ */
171
+ declare class ActionUnsupportedError extends Error {
172
+ readonly action: string;
173
+ readonly processor: string;
174
+ name: string;
175
+ constructor(action: string, processor: string, message: string);
176
+ }
130
177
  //#endregion
131
- export { type AggregateQuery, type AggregateResult, Client, ClientError, type ClientOptions, type ClientValidationError, type ClientValidator, type DataOf, type FieldMeta, type FilterExpr, type IdOf, type MetaResponse, type NavOf, type OwnOf, type PageResult, type RelationInfo, type SearchIndexInfo, type ServerError, type TDbDeleteResult, type TDbInsertManyResult, type TDbInsertResult, type TDbUpdateResult, type TSerializedAnnotatedType, type TypedWithRelation, type Uniquery, type UniqueryControls, type ValidatorMode };
178
+ export { ActionNotFoundError, ActionUnsupportedError, type AggregateQuery, type AggregateResult, Client, ClientError, type ClientOptions, type ClientValidationError, type ClientValidator, type DataOf, type FieldMeta, type FilterExpr, type IdOf, type MetaResponse, type NavOf, type OwnOf, type PageResult, type RelationInfo, type SearchIndexInfo, type ServerError, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbDeleteResult, type TDbInsertManyResult, type TDbInsertResult, type TDbUpdateResult, type TSerializedAnnotatedType, type TypedWithRelation, type Uniquery, type UniqueryControls, type ValidatorMode };
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
- import { C as TDbUpdateResult, E as UniqueryControls, S as TDbInsertResult, T as Uniquery, _ as RelationInfo, b as TDbDeleteResult, c as ClientOptions, d as FilterExpr, f as IdOf, g as PageResult, h as OwnOf, i as ValidatorMode, l as DataOf, m as NavOf, n as ClientValidator, o as AggregateQuery, p as MetaResponse, s as AggregateResult, t as ClientValidationError, u as FieldMeta, v as SearchIndexInfo, w as TypedWithRelation, x as TDbInsertManyResult, y as ServerError } from "./validator-wyKVvc-q.mjs";
1
+ import { C as TDbUpdateResult, E as UniqueryControls, S as TDbInsertResult, T as Uniquery, _ as RelationInfo, b as TDbDeleteResult, c as ClientOptions, d as FilterExpr, f as IdOf, g as PageResult, h as OwnOf, i as ValidatorMode, l as DataOf, m as NavOf, n as ClientValidator, o as AggregateQuery, p as MetaResponse, s as AggregateResult, t as ClientValidationError, u as FieldMeta, v as SearchIndexInfo, w as TypedWithRelation, x as TDbInsertManyResult, y as ServerError } from "./validator-DfNMCEKa.mjs";
2
2
  import { TSerializedAnnotatedType } from "@atscript/typescript/utils";
3
3
  import { AggregateQuery as AggregateQuery$1, AggregateResult as AggregateResult$1, Uniquery as Uniquery$1, UniqueryControls as UniqueryControls$1 } from "@uniqu/core";
4
- import { TDbDeleteResult as TDbDeleteResult$1, TDbInsertManyResult as TDbInsertManyResult$1, TDbInsertResult as TDbInsertResult$1, TDbUpdateResult as TDbUpdateResult$1 } from "@atscript/db";
4
+ import { TDbActionInfo, TDbActionIntent, TDbActionLevel, TDbActionProcessor, TDbDeleteResult as TDbDeleteResult$1, TDbInsertManyResult as TDbInsertManyResult$1, TDbInsertResult as TDbInsertResult$1, TDbUpdateResult as TDbUpdateResult$1 } from "@atscript/db";
5
5
 
6
6
  //#region src/client.d.ts
7
7
  type Own<T> = OwnOf<T>;
@@ -34,6 +34,7 @@ declare class Client<T = Record<string, unknown>> {
34
34
  private readonly _baseUrl;
35
35
  private readonly _fetch;
36
36
  private readonly _headers?;
37
+ private readonly _navigate?;
37
38
  private _metaPromise?;
38
39
  private _validatorPromise?;
39
40
  constructor(path: string, opts?: ClientOptions);
@@ -90,6 +91,28 @@ declare class Client<T = Record<string, unknown>> {
90
91
  * `GET /meta` — table/view metadata (cached after first call).
91
92
  */
92
93
  meta(): Promise<MetaResponse>;
94
+ /**
95
+ * Invoke a declared action by name. Resolves the action descriptor from the
96
+ * cached `/meta` response, then dispatches based on `processor`:
97
+ *
98
+ * - `'backend'` → POST `pk` (or `pks`) as a JSON body to the action's path
99
+ * and return the parsed server response. The HTTP method is always POST.
100
+ * - `'navigate'` → for `level: 'row'`, substitute `$1` in `value` with the
101
+ * PK (URL-encoded; composite PKs are URL-encoded per field and joined
102
+ * with `/`); for `level: 'rows'` or `'table'`, navigate to `value`
103
+ * verbatim. The default navigator (browser only) calls
104
+ * `window.location.assign(url)`. Provide `ClientOptions.navigate` to
105
+ * integrate with a SPA router.
106
+ * - `'custom'` → throw {@link ActionUnsupportedError}; UI-dispatched events
107
+ * are the application's responsibility, not the client's.
108
+ *
109
+ * Throws {@link ActionNotFoundError} when the action is not present in `/meta`.
110
+ *
111
+ * For `level: 'rows'`, `pk` must be an array. If a non-array is supplied
112
+ * for a `'rows'` action it is wrapped into a single-element array — the
113
+ * server-side `@DbActionPKs()` resolver requires an array body.
114
+ */
115
+ action(name: string, pk?: unknown): Promise<unknown>;
93
116
  /**
94
117
  * Returns a lazily-initialized {@link ClientValidator} backed by the `/meta` type.
95
118
  * Useful for accessing `flatMap` and `navFields` (e.g. for form generation).
@@ -97,11 +120,17 @@ declare class Client<T = Record<string, unknown>> {
97
120
  getValidator(): Promise<ClientValidator>;
98
121
  private _validateData;
99
122
  private _getValidator;
123
+ private _buildActionBody;
124
+ private _interpolateNavigateUrl;
125
+ private _dispatchNavigate;
126
+ private _postAction;
100
127
  private _idToParams;
101
128
  private _getOrNull;
102
129
  private _get;
103
130
  private _resolveHeaders;
104
131
  private _request;
132
+ private _buildInit;
133
+ private _send;
105
134
  }
106
135
  //#endregion
107
136
  //#region src/client-error.d.ts
@@ -127,5 +156,23 @@ declare class ClientError extends Error {
127
156
  details?: unknown[];
128
157
  }[];
129
158
  }
159
+ /** Thrown by `Client.action()` when the action name is not present in `/meta`. */
160
+ declare class ActionNotFoundError extends Error {
161
+ readonly action: string;
162
+ name: string;
163
+ constructor(action: string);
164
+ }
165
+ /**
166
+ * Thrown by `Client.action()` for actions that cannot be invoked through
167
+ * the client — currently `processor: 'custom'` (UI-dispatched events,
168
+ * which the application is responsible for handling) and `processor: 'navigate'`
169
+ * when no browser environment and no `navigate` option are configured.
170
+ */
171
+ declare class ActionUnsupportedError extends Error {
172
+ readonly action: string;
173
+ readonly processor: string;
174
+ name: string;
175
+ constructor(action: string, processor: string, message: string);
176
+ }
130
177
  //#endregion
131
- export { type AggregateQuery, type AggregateResult, Client, ClientError, type ClientOptions, type ClientValidationError, type ClientValidator, type DataOf, type FieldMeta, type FilterExpr, type IdOf, type MetaResponse, type NavOf, type OwnOf, type PageResult, type RelationInfo, type SearchIndexInfo, type ServerError, type TDbDeleteResult, type TDbInsertManyResult, type TDbInsertResult, type TDbUpdateResult, type TSerializedAnnotatedType, type TypedWithRelation, type Uniquery, type UniqueryControls, type ValidatorMode };
178
+ export { ActionNotFoundError, ActionUnsupportedError, type AggregateQuery, type AggregateResult, Client, ClientError, type ClientOptions, type ClientValidationError, type ClientValidator, type DataOf, type FieldMeta, type FilterExpr, type IdOf, type MetaResponse, type NavOf, type OwnOf, type PageResult, type RelationInfo, type SearchIndexInfo, type ServerError, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbDeleteResult, type TDbInsertManyResult, type TDbInsertResult, type TDbUpdateResult, type TSerializedAnnotatedType, type TypedWithRelation, type Uniquery, type UniqueryControls, type ValidatorMode };
package/dist/index.mjs CHANGED
@@ -16,6 +16,28 @@ var ClientError = class extends Error {
16
16
  return this.body.errors ?? [];
17
17
  }
18
18
  };
19
+ /** Thrown by `Client.action()` when the action name is not present in `/meta`. */
20
+ var ActionNotFoundError = class extends Error {
21
+ name = "ActionNotFoundError";
22
+ constructor(action) {
23
+ super(`Action "${action}" is not declared on this controller`);
24
+ this.action = action;
25
+ }
26
+ };
27
+ /**
28
+ * Thrown by `Client.action()` for actions that cannot be invoked through
29
+ * the client — currently `processor: 'custom'` (UI-dispatched events,
30
+ * which the application is responsible for handling) and `processor: 'navigate'`
31
+ * when no browser environment and no `navigate` option are configured.
32
+ */
33
+ var ActionUnsupportedError = class extends Error {
34
+ name = "ActionUnsupportedError";
35
+ constructor(action, processor, message) {
36
+ super(message);
37
+ this.action = action;
38
+ this.processor = processor;
39
+ }
40
+ };
19
41
  //#endregion
20
42
  //#region src/client.ts
21
43
  /**
@@ -44,6 +66,7 @@ var Client = class {
44
66
  _baseUrl;
45
67
  _fetch;
46
68
  _headers;
69
+ _navigate;
47
70
  _metaPromise;
48
71
  _validatorPromise;
49
72
  constructor(path, opts) {
@@ -51,6 +74,7 @@ var Client = class {
51
74
  this._baseUrl = opts?.baseUrl ?? "";
52
75
  this._fetch = opts?.fetch ?? globalThis.fetch.bind(globalThis);
53
76
  this._headers = opts?.headers;
77
+ this._navigate = opts?.navigate;
54
78
  }
55
79
  /**
56
80
  * `GET /query` — query records with typed filter, sort, select, and relations.
@@ -137,6 +161,39 @@ var Client = class {
137
161
  return this._metaPromise;
138
162
  }
139
163
  /**
164
+ * Invoke a declared action by name. Resolves the action descriptor from the
165
+ * cached `/meta` response, then dispatches based on `processor`:
166
+ *
167
+ * - `'backend'` → POST `pk` (or `pks`) as a JSON body to the action's path
168
+ * and return the parsed server response. The HTTP method is always POST.
169
+ * - `'navigate'` → for `level: 'row'`, substitute `$1` in `value` with the
170
+ * PK (URL-encoded; composite PKs are URL-encoded per field and joined
171
+ * with `/`); for `level: 'rows'` or `'table'`, navigate to `value`
172
+ * verbatim. The default navigator (browser only) calls
173
+ * `window.location.assign(url)`. Provide `ClientOptions.navigate` to
174
+ * integrate with a SPA router.
175
+ * - `'custom'` → throw {@link ActionUnsupportedError}; UI-dispatched events
176
+ * are the application's responsibility, not the client's.
177
+ *
178
+ * Throws {@link ActionNotFoundError} when the action is not present in `/meta`.
179
+ *
180
+ * For `level: 'rows'`, `pk` must be an array. If a non-array is supplied
181
+ * for a `'rows'` action it is wrapped into a single-element array — the
182
+ * server-side `@DbActionPKs()` resolver requires an array body.
183
+ */
184
+ async action(name, pk) {
185
+ const action = (await this.meta()).actions.find((a) => a.name === name);
186
+ if (!action) throw new ActionNotFoundError(name);
187
+ if (action.processor === "custom") throw new ActionUnsupportedError(name, "custom", `Action "${name}" has processor "custom" — applications must dispatch custom actions themselves; the client cannot.`);
188
+ if (action.processor === "navigate") {
189
+ const url = this._interpolateNavigateUrl(action, pk);
190
+ await this._dispatchNavigate(action, url);
191
+ return;
192
+ }
193
+ const body = this._buildActionBody(action, pk);
194
+ return this._postAction(action, body);
195
+ }
196
+ /**
140
197
  * Returns a lazily-initialized {@link ClientValidator} backed by the `/meta` type.
141
198
  * Useful for accessing `flatMap` and `navFields` (e.g. for form generation).
142
199
  */
@@ -153,6 +210,34 @@ var Client = class {
153
210
  });
154
211
  return this._validatorPromise;
155
212
  }
213
+ _buildActionBody(action, pk) {
214
+ if (action.level === "table") return void 0;
215
+ if (action.level !== "rows") return pk;
216
+ if (Array.isArray(pk)) return pk;
217
+ return pk === void 0 ? [] : [pk];
218
+ }
219
+ _interpolateNavigateUrl(action, pk) {
220
+ if (action.level !== "row") return action.value;
221
+ if (pk === void 0) return action.value;
222
+ return action.value.replace(/\$1/g, encodeNavigatePk(pk));
223
+ }
224
+ async _dispatchNavigate(action, url) {
225
+ if (this._navigate) {
226
+ await this._navigate(url);
227
+ return;
228
+ }
229
+ const loc = globalThis.location;
230
+ if (loc?.assign) {
231
+ loc.assign(url);
232
+ return;
233
+ }
234
+ throw new ActionUnsupportedError(action.name, "navigate", `Action "${action.name}" is processor: 'navigate' but no browser is available and no \`navigate\` option was provided to Client.`);
235
+ }
236
+ async _postAction(action, body) {
237
+ const url = `${this._baseUrl}${action.value}`;
238
+ const init = await this._buildInit("POST", body);
239
+ return this._send(url, init, true);
240
+ }
156
241
  _idToParams(id) {
157
242
  const params = new URLSearchParams();
158
243
  for (const [k, v] of Object.entries(id)) params.set(k, String(v));
@@ -178,6 +263,10 @@ var Client = class {
178
263
  async _request(method, endpoint, body) {
179
264
  const sep = endpoint && !endpoint.startsWith("?") ? "/" : "";
180
265
  const url = `${this._baseUrl}${this._path}${sep}${endpoint}`;
266
+ const init = await this._buildInit(method, body);
267
+ return this._send(url, init, false);
268
+ }
269
+ async _buildInit(method, body) {
181
270
  const headers = { ...await this._resolveHeaders() };
182
271
  const init = {
183
272
  method,
@@ -187,6 +276,9 @@ var Client = class {
187
276
  headers["Content-Type"] = "application/json";
188
277
  init.body = JSON.stringify(body);
189
278
  }
279
+ return init;
280
+ }
281
+ async _send(url, init, allowEmpty) {
190
282
  const res = await this._fetch(url, init);
191
283
  if (!res.ok) {
192
284
  let errorBody;
@@ -200,8 +292,24 @@ var Client = class {
200
292
  }
201
293
  throw new ClientError(res.status, errorBody);
202
294
  }
203
- return res.json();
295
+ if (!allowEmpty) return res.json();
296
+ if (res.status === 204 || res.headers.get("content-length") === "0") return void 0;
297
+ try {
298
+ return await res.json();
299
+ } catch {
300
+ return;
301
+ }
204
302
  }
205
303
  };
304
+ /**
305
+ * Encode a row PK for substitution into a `processor: 'navigate'` URL template.
306
+ * Scalars are URL-encoded directly; composite PK objects have each value
307
+ * URL-encoded and joined with `/` in object-key order (which mirrors
308
+ * `primaryKeys` for the table).
309
+ */
310
+ function encodeNavigatePk(pk) {
311
+ if (pk === null || pk === void 0) return "";
312
+ return (typeof pk === "object" ? Object.values(pk) : [pk]).map((v) => encodeURIComponent(String(v))).join("/");
313
+ }
206
314
  //#endregion
207
- export { Client, ClientError };
315
+ export { ActionNotFoundError, ActionUnsupportedError, Client, ClientError };
@@ -21,6 +21,17 @@ interface ClientOptions {
21
21
  * @example "https://api.example.com"
22
22
  */
23
23
  baseUrl?: string;
24
+ /**
25
+ * Override for `processor: 'navigate'` action dispatch. When `Client.action()`
26
+ * resolves a navigate action, this hook is invoked with the interpolated
27
+ * URL. Default behaviour (browser only) calls `window.location.assign(url)`.
28
+ *
29
+ * Provide a custom navigator to integrate with a SPA router:
30
+ * ```typescript
31
+ * new Client('/api/users', { navigate: (url) => router.push(url) })
32
+ * ```
33
+ */
34
+ navigate?: (url: string) => void | Promise<void>;
24
35
  }
25
36
  /** Search index metadata from the server. */
26
37
  type SearchIndexInfo = TSearchIndexInfo;
@@ -21,6 +21,17 @@ interface ClientOptions {
21
21
  * @example "https://api.example.com"
22
22
  */
23
23
  baseUrl?: string;
24
+ /**
25
+ * Override for `processor: 'navigate'` action dispatch. When `Client.action()`
26
+ * resolves a navigate action, this hook is invoked with the interpolated
27
+ * URL. Default behaviour (browser only) calls `window.location.assign(url)`.
28
+ *
29
+ * Provide a custom navigator to integrate with a SPA router:
30
+ * ```typescript
31
+ * new Client('/api/users', { navigate: (url) => router.push(url) })
32
+ * ```
33
+ */
34
+ navigate?: (url: string) => void | Promise<void>;
24
35
  }
25
36
  /** Search index metadata from the server. */
26
37
  type SearchIndexInfo = TSearchIndexInfo;
@@ -1,2 +1,2 @@
1
- import { a as createClientValidator, i as ValidatorMode, n as ClientValidator, r as DbValidationContext, t as ClientValidationError } from "./validator-DfU6oJir.cjs";
1
+ import { a as createClientValidator, i as ValidatorMode, n as ClientValidator, r as DbValidationContext, t as ClientValidationError } from "./validator-DNm9kCoq.cjs";
2
2
  export { ClientValidationError, ClientValidator, DbValidationContext, ValidatorMode, createClientValidator };
@@ -1,2 +1,2 @@
1
- import { a as createClientValidator, i as ValidatorMode, n as ClientValidator, r as DbValidationContext, t as ClientValidationError } from "./validator-wyKVvc-q.mjs";
1
+ import { a as createClientValidator, i as ValidatorMode, n as ClientValidator, r as DbValidationContext, t as ClientValidationError } from "./validator-DfNMCEKa.mjs";
2
2
  export { ClientValidationError, ClientValidator, DbValidationContext, ValidatorMode, createClientValidator };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-client",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "description": "Browser-compatible HTTP client for @atscript/moost-db REST endpoints.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -46,15 +46,15 @@
46
46
  "@uniqu/url": "^0.1.5"
47
47
  },
48
48
  "devDependencies": {
49
- "@atscript/core": "^0.1.48",
50
- "@atscript/typescript": "^0.1.48",
49
+ "@atscript/core": "^0.1.50",
50
+ "@atscript/typescript": "^0.1.50",
51
51
  "@uniqu/core": "^0.1.5",
52
- "unplugin-atscript": "^0.1.48",
53
- "@atscript/db": "0.1.54"
52
+ "unplugin-atscript": "^0.1.50",
53
+ "@atscript/db": "0.1.56"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "@atscript/db": "^0.1.44",
57
- "@atscript/typescript": "^0.1.48"
57
+ "@atscript/typescript": "^0.1.50"
58
58
  },
59
59
  "scripts": {
60
60
  "postinstall": "asc -f dts",