@atscript/db-client 0.1.57 → 0.1.59

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
+ /**
21
+ * Typed marker thrown by `Client._send` when the server response body's
22
+ * `name === 'ActionDisabledError'`. The transport / status / base body are
23
+ * identical to a generic `ClientError`; this subclass adds typed accessors
24
+ * so consumers can write `catch (e) { if (e instanceof ActionDisabledError) … }`
25
+ * to access `action` / `id` / `ids` without indexing into `body`.
26
+ */
27
+ var ActionDisabledError = class extends ClientError {
28
+ name = "ActionDisabledError";
29
+ /** The `@DbAction` name that rejected the request. */
30
+ get action() {
31
+ return this.body.action;
32
+ }
33
+ /** Present only for `'row'`-level rejections. */
34
+ get id() {
35
+ return this.body.id;
36
+ }
37
+ /** Present only for `'rows'`-level rejections (full list of failing IDs). */
38
+ get ids() {
39
+ return this.body.ids;
40
+ }
41
+ };
20
42
  /** Thrown by `Client.action()` when the action name is not present in `/meta`. */
21
43
  var ActionNotFoundError = class extends Error {
22
44
  name = "ActionNotFoundError";
@@ -79,6 +101,9 @@ var Client = class {
79
101
  }
80
102
  /**
81
103
  * `GET /query` — query records with typed filter, sort, select, and relations.
104
+ *
105
+ * The response type narrows by the literal `$with` array in `query` —
106
+ * relations not listed in `$with` are stripped from the row type.
82
107
  */
83
108
  async query(query) {
84
109
  return this._get("query", query);
@@ -100,6 +125,9 @@ var Client = class {
100
125
  }
101
126
  /**
102
127
  * `GET /pages` — paginated query with typed filter and relations.
128
+ *
129
+ * Response rows narrow by the literal `$with` array — same algebra as
130
+ * {@link query}.
103
131
  */
104
132
  async pages(query, page = 1, size = 10) {
105
133
  return this._get("pages", {
@@ -114,7 +142,8 @@ var Client = class {
114
142
  /**
115
143
  * `GET /one/:id` or `GET /one?k1=v1&k2=v2` — single record by primary key.
116
144
  *
117
- * Returns `null` on 404.
145
+ * Returns `null` on 404. Response narrows by the literal `$with` array in
146
+ * `query.controls` — same algebra as {@link query}.
118
147
  */
119
148
  async one(id, query) {
120
149
  const controlStr = query?.controls ? (0, _uniqu_url_builder.buildUrl)({ controls: query.controls }) : "";
@@ -165,33 +194,45 @@ var Client = class {
165
194
  * Invoke a declared action by name. Resolves the action descriptor from the
166
195
  * cached `/meta` response, then dispatches based on `processor`:
167
196
  *
168
- * - `'backend'` → POST `pk` (or `pks`) as a JSON body to the action's path
197
+ * - `'backend'` → POST the identifier as a JSON body to the action's path
169
198
  * and return the parsed server response. The HTTP method is always POST.
170
199
  * - `'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.
200
+ * identifier values, walking `meta.preferredId` field order (each value
201
+ * URL-encoded, compound IDs joined with `/`); for `level: 'rows'` or
202
+ * `'table'`, navigate to `value` verbatim. The default navigator (browser
203
+ * only) calls `window.location.assign(url)`. Provide
204
+ * `ClientOptions.navigate` to integrate with a SPA router.
176
205
  * - `'custom'` → throw {@link ActionUnsupportedError}; UI-dispatched events
177
206
  * are the application's responsibility, not the client's.
178
207
  *
179
208
  * Throws {@link ActionNotFoundError} when the action is not present in `/meta`.
180
209
  *
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.
210
+ * **Identifier shape (server contract).** `id` is always an object (single)
211
+ * or array of objects (multi) never a scalar. Each object's field set
212
+ * must exactly match one legitimate identification on the table (PK or any
213
+ * `@db.index.unique` group). Even single-field PK tables send `{ id: 'abc' }`,
214
+ * not `'abc'`. `level: 'table'` actions take no identifier (`undefined`).
215
+ *
216
+ * The TypeScript signature widens to `Partial<Own<T>>` because the server's
217
+ * exact-match validation cannot be expressed at the type level. Mismatched
218
+ * field sets produce HTTP 400; obvious type errors (scalars, `null`) are
219
+ * caught at compile time when `T` is typed.
220
+ *
221
+ * @typeParam R Caller-asserted return shape from the action handler. The
222
+ * server returns whatever the handler emits (commonly
223
+ * `{ message?: string, ... }`); the client cannot validate.
184
224
  */
185
- async action(name, pk) {
186
- const action = (await this.meta()).actions.find((a) => a.name === name);
225
+ async action(name, id) {
226
+ const meta = await this.meta();
227
+ const action = meta.actions.find((a) => a.name === name);
187
228
  if (!action) throw new ActionNotFoundError(name);
188
229
  if (action.processor === "custom") throw new ActionUnsupportedError(name, "custom", `Action "${name}" has processor "custom" — applications must dispatch custom actions themselves; the client cannot.`);
189
230
  if (action.processor === "navigate") {
190
- const url = this._interpolateNavigateUrl(action, pk);
231
+ const url = this._interpolateNavigateUrl(action, id, meta.preferredId);
191
232
  await this._dispatchNavigate(action, url);
192
233
  return;
193
234
  }
194
- const body = this._buildActionBody(action, pk);
235
+ const body = this._buildActionBody(action, id);
195
236
  return this._postAction(action, body);
196
237
  }
197
238
  /**
@@ -211,16 +252,21 @@ var Client = class {
211
252
  });
212
253
  return this._validatorPromise;
213
254
  }
214
- _buildActionBody(action, pk) {
255
+ _buildActionBody(action, id) {
215
256
  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];
257
+ if (action.level === "rows") {
258
+ if (id === void 0) return [];
259
+ if (!Array.isArray(id)) throw new TypeError(`client.action("${action.name}"): rows-level actions require an array of identifier objects; received ${describeShape(id)}.`);
260
+ return id;
261
+ }
262
+ if (id !== null && typeof id === "object" && !Array.isArray(id)) return id;
263
+ throw new TypeError(`client.action("${action.name}"): row-level actions require an identifier object; received ${describeShape(id)}.`);
219
264
  }
220
- _interpolateNavigateUrl(action, pk) {
265
+ _interpolateNavigateUrl(action, id, preferredId) {
221
266
  if (action.level !== "row") return action.value;
222
- if (pk === void 0) return action.value;
223
- return action.value.replace(/\$1/g, encodeNavigatePk(pk));
267
+ if (id === void 0) return action.value;
268
+ if (id === null || typeof id !== "object" || Array.isArray(id)) throw new TypeError(`client.action("${action.name}"): row-level navigate actions require an identifier object; received ${describeShape(id)}.`);
269
+ return action.value.replace(/\$1/g, encodeNavigateId(id, preferredId));
224
270
  }
225
271
  async _dispatchNavigate(action, url) {
226
272
  if (this._navigate) {
@@ -291,6 +337,7 @@ var Client = class {
291
337
  statusCode: res.status
292
338
  };
293
339
  }
340
+ if (errorBody.name === "ActionDisabledError") throw new ActionDisabledError(res.status, errorBody);
294
341
  throw new ClientError(res.status, errorBody);
295
342
  }
296
343
  if (!allowEmpty) return res.json();
@@ -303,16 +350,21 @@ var Client = class {
303
350
  }
304
351
  };
305
352
  /**
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).
353
+ * Encode a row identifier for substitution into a `processor: 'navigate'` URL template.
354
+ * The values are URL-encoded and joined with `/` in `meta.preferredId` field
355
+ * declaration order NOT in object-key insertion order (which is unstable
356
+ * across callers).
310
357
  */
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("/");
358
+ function encodeNavigateId(id, preferredId) {
359
+ return preferredId.map((f) => encodeURIComponent(String(id[f]))).join("/");
360
+ }
361
+ function describeShape(value) {
362
+ if (value === null) return "null";
363
+ if (Array.isArray(value)) return "array";
364
+ return typeof value;
314
365
  }
315
366
  //#endregion
367
+ exports.ActionDisabledError = ActionDisabledError;
316
368
  exports.ActionNotFoundError = ActionNotFoundError;
317
369
  exports.ActionUnsupportedError = ActionUnsupportedError;
318
370
  exports.Client = Client;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
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";
1
+ import { C as TDbInsertManyResult, D as Uniquery, E as TypedWithRelation, O as UniqueryControls, S as TDbDeleteResult, T as TDbUpdateResult, _ as OwnOf, b as SearchIndexInfo, c as AtscriptClientShape, d as DataOf, f as FieldMeta, g as NavOf, h as MetaResponse, i as ValidatorMode, l as ClientOptions, m as IdOf, n as ClientValidator, o as AggregateQuery, p as FilterExpr, s as AggregateResult, t as ClientValidationError, u as ClientResponse, v as PageResult, w as TDbInsertResult, x as ServerError, y as RelationInfo } from "./validator-BminFmAf.cjs";
2
2
  import { AggregateQuery as AggregateQuery$1, AggregateResult as AggregateResult$1, Uniquery as Uniquery$1, UniqueryControls as UniqueryControls$1 } from "@uniqu/core";
3
3
  import { TCrudOp, TCrudPermissions, 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";
@@ -8,6 +8,7 @@ type Own<T> = OwnOf<T>;
8
8
  type Nav<T> = NavOf<T>;
9
9
  type Data<T> = DataOf<T>;
10
10
  type Id<T> = IdOf<T>;
11
+ type Response<T, Q> = ClientResponse<T, Q>;
11
12
  /**
12
13
  * HTTP client for moost-db REST endpoints.
13
14
  *
@@ -29,7 +30,7 @@ type Id<T> = IdOf<T>;
29
30
  * const page = await users.pages({ filter: { active: true } }, 1, 20)
30
31
  * ```
31
32
  */
32
- declare class Client<T = Record<string, unknown>> {
33
+ declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
33
34
  private readonly _path;
34
35
  private readonly _baseUrl;
35
36
  private readonly _fetch;
@@ -40,8 +41,11 @@ declare class Client<T = Record<string, unknown>> {
40
41
  constructor(path: string, opts?: ClientOptions);
41
42
  /**
42
43
  * `GET /query` — query records with typed filter, sort, select, and relations.
44
+ *
45
+ * The response type narrows by the literal `$with` array in `query` —
46
+ * relations not listed in `$with` are stripped from the row type.
43
47
  */
44
- query(query?: Uniquery$1<Own<T>, Nav<T>>): Promise<Data<T>[]>;
48
+ query<Q extends Uniquery$1<Own<T>, Nav<T>> = Uniquery$1<Own<T>, Nav<T>>>(query?: Q): Promise<Response<T, Q>[]>;
45
49
  /**
46
50
  * `GET /query` with `$count: true` — returns record count.
47
51
  */
@@ -57,16 +61,22 @@ declare class Client<T = Record<string, unknown>> {
57
61
  })[] ? AggregateResult$1<Own<T>, Q["controls"]["$select"]>[] : Record<string, unknown>[]>;
58
62
  /**
59
63
  * `GET /pages` — paginated query with typed filter and relations.
64
+ *
65
+ * Response rows narrow by the literal `$with` array — same algebra as
66
+ * {@link query}.
60
67
  */
61
- pages(query?: Uniquery$1<Own<T>, Nav<T>>, page?: number, size?: number): Promise<PageResult<Data<T>>>;
68
+ pages<Q extends Uniquery$1<Own<T>, Nav<T>> = Uniquery$1<Own<T>, Nav<T>>>(query?: Q, page?: number, size?: number): Promise<PageResult<Response<T, Q>>>;
62
69
  /**
63
70
  * `GET /one/:id` or `GET /one?k1=v1&k2=v2` — single record by primary key.
64
71
  *
65
- * Returns `null` on 404.
72
+ * Returns `null` on 404. Response narrows by the literal `$with` array in
73
+ * `query.controls` — same algebra as {@link query}.
66
74
  */
67
- one(id: Id<T>, query?: {
75
+ one<Q extends {
76
+ controls?: UniqueryControls$1<Own<T>, Nav<T>>;
77
+ } = {
68
78
  controls?: UniqueryControls$1<Own<T>, Nav<T>>;
69
- }): Promise<Data<T> | null>;
79
+ }>(id: Id<T>, query?: Q): Promise<Response<T, Q> | null>;
70
80
  /**
71
81
  * `POST /` — insert one record.
72
82
  */
@@ -95,24 +105,35 @@ declare class Client<T = Record<string, unknown>> {
95
105
  * Invoke a declared action by name. Resolves the action descriptor from the
96
106
  * cached `/meta` response, then dispatches based on `processor`:
97
107
  *
98
- * - `'backend'` → POST `pk` (or `pks`) as a JSON body to the action's path
108
+ * - `'backend'` → POST the identifier as a JSON body to the action's path
99
109
  * and return the parsed server response. The HTTP method is always POST.
100
110
  * - `'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.
111
+ * identifier values, walking `meta.preferredId` field order (each value
112
+ * URL-encoded, compound IDs joined with `/`); for `level: 'rows'` or
113
+ * `'table'`, navigate to `value` verbatim. The default navigator (browser
114
+ * only) calls `window.location.assign(url)`. Provide
115
+ * `ClientOptions.navigate` to integrate with a SPA router.
106
116
  * - `'custom'` → throw {@link ActionUnsupportedError}; UI-dispatched events
107
117
  * are the application's responsibility, not the client's.
108
118
  *
109
119
  * Throws {@link ActionNotFoundError} when the action is not present in `/meta`.
110
120
  *
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.
121
+ * **Identifier shape (server contract).** `id` is always an object (single)
122
+ * or array of objects (multi) never a scalar. Each object's field set
123
+ * must exactly match one legitimate identification on the table (PK or any
124
+ * `@db.index.unique` group). Even single-field PK tables send `{ id: 'abc' }`,
125
+ * not `'abc'`. `level: 'table'` actions take no identifier (`undefined`).
126
+ *
127
+ * The TypeScript signature widens to `Partial<Own<T>>` because the server's
128
+ * exact-match validation cannot be expressed at the type level. Mismatched
129
+ * field sets produce HTTP 400; obvious type errors (scalars, `null`) are
130
+ * caught at compile time when `T` is typed.
131
+ *
132
+ * @typeParam R Caller-asserted return shape from the action handler. The
133
+ * server returns whatever the handler emits (commonly
134
+ * `{ message?: string, ... }`); the client cannot validate.
114
135
  */
115
- action(name: string, pk?: unknown): Promise<unknown>;
136
+ action<R = unknown>(name: string, id?: Partial<Own<T>> | Partial<Own<T>>[]): Promise<R>;
116
137
  /**
117
138
  * Returns a lazily-initialized {@link ClientValidator} backed by the `/meta` type.
118
139
  * Useful for accessing `flatMap` and `navFields` (e.g. for form generation).
@@ -156,6 +177,35 @@ declare class ClientError extends Error {
156
177
  details?: unknown[];
157
178
  }[];
158
179
  }
180
+ /**
181
+ * Wire-body shape for `ActionDisabledError` responses (HTTP 409). Extends
182
+ * the base `ServerError` envelope with a `name` discriminator, the action
183
+ * name, and the offending identifier(s). The bridge between `@atscript/moost-db`'s
184
+ * server-side error and this typed client-side subclass is the wire JSON
185
+ * body — neither package depends on the other.
186
+ */
187
+ interface ActionDisabledErrorBody extends ServerError {
188
+ name: "ActionDisabledError";
189
+ action: string;
190
+ id?: Record<string, unknown>;
191
+ ids?: Record<string, unknown>[];
192
+ }
193
+ /**
194
+ * Typed marker thrown by `Client._send` when the server response body's
195
+ * `name === 'ActionDisabledError'`. The transport / status / base body are
196
+ * identical to a generic `ClientError`; this subclass adds typed accessors
197
+ * so consumers can write `catch (e) { if (e instanceof ActionDisabledError) … }`
198
+ * to access `action` / `id` / `ids` without indexing into `body`.
199
+ */
200
+ declare class ActionDisabledError extends ClientError {
201
+ name: string;
202
+ /** The `@DbAction` name that rejected the request. */
203
+ get action(): string;
204
+ /** Present only for `'row'`-level rejections. */
205
+ get id(): Record<string, unknown> | undefined;
206
+ /** Present only for `'rows'`-level rejections (full list of failing IDs). */
207
+ get ids(): Record<string, unknown>[] | undefined;
208
+ }
159
209
  /** Thrown by `Client.action()` when the action name is not present in `/meta`. */
160
210
  declare class ActionNotFoundError extends Error {
161
211
  readonly action: string;
@@ -175,4 +225,4 @@ declare class ActionUnsupportedError extends Error {
175
225
  constructor(action: string, processor: string, message: string);
176
226
  }
177
227
  //#endregion
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 TCrudOp, type TCrudPermissions, 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 };
228
+ export { ActionDisabledError, type ActionDisabledErrorBody, ActionNotFoundError, ActionUnsupportedError, type AggregateQuery, type AggregateResult, type AtscriptClientShape, Client, ClientError, type ClientOptions, type ClientResponse, 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 TCrudOp, type TCrudPermissions, 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,4 +1,4 @@
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";
1
+ import { C as TDbInsertManyResult, D as Uniquery, E as TypedWithRelation, O as UniqueryControls, S as TDbDeleteResult, T as TDbUpdateResult, _ as OwnOf, b as SearchIndexInfo, c as AtscriptClientShape, d as DataOf, f as FieldMeta, g as NavOf, h as MetaResponse, i as ValidatorMode, l as ClientOptions, m as IdOf, n as ClientValidator, o as AggregateQuery, p as FilterExpr, s as AggregateResult, t as ClientValidationError, u as ClientResponse, v as PageResult, w as TDbInsertResult, x as ServerError, y as RelationInfo } from "./validator-DkIQ_0Lc.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
4
  import { TCrudOp, TCrudPermissions, TDbActionInfo, TDbActionIntent, TDbActionLevel, TDbActionProcessor, TDbDeleteResult as TDbDeleteResult$1, TDbInsertManyResult as TDbInsertManyResult$1, TDbInsertResult as TDbInsertResult$1, TDbUpdateResult as TDbUpdateResult$1 } from "@atscript/db";
@@ -8,6 +8,7 @@ type Own<T> = OwnOf<T>;
8
8
  type Nav<T> = NavOf<T>;
9
9
  type Data<T> = DataOf<T>;
10
10
  type Id<T> = IdOf<T>;
11
+ type Response<T, Q> = ClientResponse<T, Q>;
11
12
  /**
12
13
  * HTTP client for moost-db REST endpoints.
13
14
  *
@@ -29,7 +30,7 @@ type Id<T> = IdOf<T>;
29
30
  * const page = await users.pages({ filter: { active: true } }, 1, 20)
30
31
  * ```
31
32
  */
32
- declare class Client<T = Record<string, unknown>> {
33
+ declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
33
34
  private readonly _path;
34
35
  private readonly _baseUrl;
35
36
  private readonly _fetch;
@@ -40,8 +41,11 @@ declare class Client<T = Record<string, unknown>> {
40
41
  constructor(path: string, opts?: ClientOptions);
41
42
  /**
42
43
  * `GET /query` — query records with typed filter, sort, select, and relations.
44
+ *
45
+ * The response type narrows by the literal `$with` array in `query` —
46
+ * relations not listed in `$with` are stripped from the row type.
43
47
  */
44
- query(query?: Uniquery$1<Own<T>, Nav<T>>): Promise<Data<T>[]>;
48
+ query<Q extends Uniquery$1<Own<T>, Nav<T>> = Uniquery$1<Own<T>, Nav<T>>>(query?: Q): Promise<Response<T, Q>[]>;
45
49
  /**
46
50
  * `GET /query` with `$count: true` — returns record count.
47
51
  */
@@ -57,16 +61,22 @@ declare class Client<T = Record<string, unknown>> {
57
61
  })[] ? AggregateResult$1<Own<T>, Q["controls"]["$select"]>[] : Record<string, unknown>[]>;
58
62
  /**
59
63
  * `GET /pages` — paginated query with typed filter and relations.
64
+ *
65
+ * Response rows narrow by the literal `$with` array — same algebra as
66
+ * {@link query}.
60
67
  */
61
- pages(query?: Uniquery$1<Own<T>, Nav<T>>, page?: number, size?: number): Promise<PageResult<Data<T>>>;
68
+ pages<Q extends Uniquery$1<Own<T>, Nav<T>> = Uniquery$1<Own<T>, Nav<T>>>(query?: Q, page?: number, size?: number): Promise<PageResult<Response<T, Q>>>;
62
69
  /**
63
70
  * `GET /one/:id` or `GET /one?k1=v1&k2=v2` — single record by primary key.
64
71
  *
65
- * Returns `null` on 404.
72
+ * Returns `null` on 404. Response narrows by the literal `$with` array in
73
+ * `query.controls` — same algebra as {@link query}.
66
74
  */
67
- one(id: Id<T>, query?: {
75
+ one<Q extends {
76
+ controls?: UniqueryControls$1<Own<T>, Nav<T>>;
77
+ } = {
68
78
  controls?: UniqueryControls$1<Own<T>, Nav<T>>;
69
- }): Promise<Data<T> | null>;
79
+ }>(id: Id<T>, query?: Q): Promise<Response<T, Q> | null>;
70
80
  /**
71
81
  * `POST /` — insert one record.
72
82
  */
@@ -95,24 +105,35 @@ declare class Client<T = Record<string, unknown>> {
95
105
  * Invoke a declared action by name. Resolves the action descriptor from the
96
106
  * cached `/meta` response, then dispatches based on `processor`:
97
107
  *
98
- * - `'backend'` → POST `pk` (or `pks`) as a JSON body to the action's path
108
+ * - `'backend'` → POST the identifier as a JSON body to the action's path
99
109
  * and return the parsed server response. The HTTP method is always POST.
100
110
  * - `'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.
111
+ * identifier values, walking `meta.preferredId` field order (each value
112
+ * URL-encoded, compound IDs joined with `/`); for `level: 'rows'` or
113
+ * `'table'`, navigate to `value` verbatim. The default navigator (browser
114
+ * only) calls `window.location.assign(url)`. Provide
115
+ * `ClientOptions.navigate` to integrate with a SPA router.
106
116
  * - `'custom'` → throw {@link ActionUnsupportedError}; UI-dispatched events
107
117
  * are the application's responsibility, not the client's.
108
118
  *
109
119
  * Throws {@link ActionNotFoundError} when the action is not present in `/meta`.
110
120
  *
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.
121
+ * **Identifier shape (server contract).** `id` is always an object (single)
122
+ * or array of objects (multi) never a scalar. Each object's field set
123
+ * must exactly match one legitimate identification on the table (PK or any
124
+ * `@db.index.unique` group). Even single-field PK tables send `{ id: 'abc' }`,
125
+ * not `'abc'`. `level: 'table'` actions take no identifier (`undefined`).
126
+ *
127
+ * The TypeScript signature widens to `Partial<Own<T>>` because the server's
128
+ * exact-match validation cannot be expressed at the type level. Mismatched
129
+ * field sets produce HTTP 400; obvious type errors (scalars, `null`) are
130
+ * caught at compile time when `T` is typed.
131
+ *
132
+ * @typeParam R Caller-asserted return shape from the action handler. The
133
+ * server returns whatever the handler emits (commonly
134
+ * `{ message?: string, ... }`); the client cannot validate.
114
135
  */
115
- action(name: string, pk?: unknown): Promise<unknown>;
136
+ action<R = unknown>(name: string, id?: Partial<Own<T>> | Partial<Own<T>>[]): Promise<R>;
116
137
  /**
117
138
  * Returns a lazily-initialized {@link ClientValidator} backed by the `/meta` type.
118
139
  * Useful for accessing `flatMap` and `navFields` (e.g. for form generation).
@@ -156,6 +177,35 @@ declare class ClientError extends Error {
156
177
  details?: unknown[];
157
178
  }[];
158
179
  }
180
+ /**
181
+ * Wire-body shape for `ActionDisabledError` responses (HTTP 409). Extends
182
+ * the base `ServerError` envelope with a `name` discriminator, the action
183
+ * name, and the offending identifier(s). The bridge between `@atscript/moost-db`'s
184
+ * server-side error and this typed client-side subclass is the wire JSON
185
+ * body — neither package depends on the other.
186
+ */
187
+ interface ActionDisabledErrorBody extends ServerError {
188
+ name: "ActionDisabledError";
189
+ action: string;
190
+ id?: Record<string, unknown>;
191
+ ids?: Record<string, unknown>[];
192
+ }
193
+ /**
194
+ * Typed marker thrown by `Client._send` when the server response body's
195
+ * `name === 'ActionDisabledError'`. The transport / status / base body are
196
+ * identical to a generic `ClientError`; this subclass adds typed accessors
197
+ * so consumers can write `catch (e) { if (e instanceof ActionDisabledError) … }`
198
+ * to access `action` / `id` / `ids` without indexing into `body`.
199
+ */
200
+ declare class ActionDisabledError extends ClientError {
201
+ name: string;
202
+ /** The `@DbAction` name that rejected the request. */
203
+ get action(): string;
204
+ /** Present only for `'row'`-level rejections. */
205
+ get id(): Record<string, unknown> | undefined;
206
+ /** Present only for `'rows'`-level rejections (full list of failing IDs). */
207
+ get ids(): Record<string, unknown>[] | undefined;
208
+ }
159
209
  /** Thrown by `Client.action()` when the action name is not present in `/meta`. */
160
210
  declare class ActionNotFoundError extends Error {
161
211
  readonly action: string;
@@ -175,4 +225,4 @@ declare class ActionUnsupportedError extends Error {
175
225
  constructor(action: string, processor: string, message: string);
176
226
  }
177
227
  //#endregion
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 TCrudOp, type TCrudPermissions, 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 };
228
+ export { ActionDisabledError, type ActionDisabledErrorBody, ActionNotFoundError, ActionUnsupportedError, type AggregateQuery, type AggregateResult, type AtscriptClientShape, Client, ClientError, type ClientOptions, type ClientResponse, 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 TCrudOp, type TCrudPermissions, 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
+ /**
20
+ * Typed marker thrown by `Client._send` when the server response body's
21
+ * `name === 'ActionDisabledError'`. The transport / status / base body are
22
+ * identical to a generic `ClientError`; this subclass adds typed accessors
23
+ * so consumers can write `catch (e) { if (e instanceof ActionDisabledError) … }`
24
+ * to access `action` / `id` / `ids` without indexing into `body`.
25
+ */
26
+ var ActionDisabledError = class extends ClientError {
27
+ name = "ActionDisabledError";
28
+ /** The `@DbAction` name that rejected the request. */
29
+ get action() {
30
+ return this.body.action;
31
+ }
32
+ /** Present only for `'row'`-level rejections. */
33
+ get id() {
34
+ return this.body.id;
35
+ }
36
+ /** Present only for `'rows'`-level rejections (full list of failing IDs). */
37
+ get ids() {
38
+ return this.body.ids;
39
+ }
40
+ };
19
41
  /** Thrown by `Client.action()` when the action name is not present in `/meta`. */
20
42
  var ActionNotFoundError = class extends Error {
21
43
  name = "ActionNotFoundError";
@@ -78,6 +100,9 @@ var Client = class {
78
100
  }
79
101
  /**
80
102
  * `GET /query` — query records with typed filter, sort, select, and relations.
103
+ *
104
+ * The response type narrows by the literal `$with` array in `query` —
105
+ * relations not listed in `$with` are stripped from the row type.
81
106
  */
82
107
  async query(query) {
83
108
  return this._get("query", query);
@@ -99,6 +124,9 @@ var Client = class {
99
124
  }
100
125
  /**
101
126
  * `GET /pages` — paginated query with typed filter and relations.
127
+ *
128
+ * Response rows narrow by the literal `$with` array — same algebra as
129
+ * {@link query}.
102
130
  */
103
131
  async pages(query, page = 1, size = 10) {
104
132
  return this._get("pages", {
@@ -113,7 +141,8 @@ var Client = class {
113
141
  /**
114
142
  * `GET /one/:id` or `GET /one?k1=v1&k2=v2` — single record by primary key.
115
143
  *
116
- * Returns `null` on 404.
144
+ * Returns `null` on 404. Response narrows by the literal `$with` array in
145
+ * `query.controls` — same algebra as {@link query}.
117
146
  */
118
147
  async one(id, query) {
119
148
  const controlStr = query?.controls ? buildUrl({ controls: query.controls }) : "";
@@ -164,33 +193,45 @@ var Client = class {
164
193
  * Invoke a declared action by name. Resolves the action descriptor from the
165
194
  * cached `/meta` response, then dispatches based on `processor`:
166
195
  *
167
- * - `'backend'` → POST `pk` (or `pks`) as a JSON body to the action's path
196
+ * - `'backend'` → POST the identifier as a JSON body to the action's path
168
197
  * and return the parsed server response. The HTTP method is always POST.
169
198
  * - `'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.
199
+ * identifier values, walking `meta.preferredId` field order (each value
200
+ * URL-encoded, compound IDs joined with `/`); for `level: 'rows'` or
201
+ * `'table'`, navigate to `value` verbatim. The default navigator (browser
202
+ * only) calls `window.location.assign(url)`. Provide
203
+ * `ClientOptions.navigate` to integrate with a SPA router.
175
204
  * - `'custom'` → throw {@link ActionUnsupportedError}; UI-dispatched events
176
205
  * are the application's responsibility, not the client's.
177
206
  *
178
207
  * Throws {@link ActionNotFoundError} when the action is not present in `/meta`.
179
208
  *
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.
209
+ * **Identifier shape (server contract).** `id` is always an object (single)
210
+ * or array of objects (multi) never a scalar. Each object's field set
211
+ * must exactly match one legitimate identification on the table (PK or any
212
+ * `@db.index.unique` group). Even single-field PK tables send `{ id: 'abc' }`,
213
+ * not `'abc'`. `level: 'table'` actions take no identifier (`undefined`).
214
+ *
215
+ * The TypeScript signature widens to `Partial<Own<T>>` because the server's
216
+ * exact-match validation cannot be expressed at the type level. Mismatched
217
+ * field sets produce HTTP 400; obvious type errors (scalars, `null`) are
218
+ * caught at compile time when `T` is typed.
219
+ *
220
+ * @typeParam R Caller-asserted return shape from the action handler. The
221
+ * server returns whatever the handler emits (commonly
222
+ * `{ message?: string, ... }`); the client cannot validate.
183
223
  */
184
- async action(name, pk) {
185
- const action = (await this.meta()).actions.find((a) => a.name === name);
224
+ async action(name, id) {
225
+ const meta = await this.meta();
226
+ const action = meta.actions.find((a) => a.name === name);
186
227
  if (!action) throw new ActionNotFoundError(name);
187
228
  if (action.processor === "custom") throw new ActionUnsupportedError(name, "custom", `Action "${name}" has processor "custom" — applications must dispatch custom actions themselves; the client cannot.`);
188
229
  if (action.processor === "navigate") {
189
- const url = this._interpolateNavigateUrl(action, pk);
230
+ const url = this._interpolateNavigateUrl(action, id, meta.preferredId);
190
231
  await this._dispatchNavigate(action, url);
191
232
  return;
192
233
  }
193
- const body = this._buildActionBody(action, pk);
234
+ const body = this._buildActionBody(action, id);
194
235
  return this._postAction(action, body);
195
236
  }
196
237
  /**
@@ -210,16 +251,21 @@ var Client = class {
210
251
  });
211
252
  return this._validatorPromise;
212
253
  }
213
- _buildActionBody(action, pk) {
254
+ _buildActionBody(action, id) {
214
255
  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];
256
+ if (action.level === "rows") {
257
+ if (id === void 0) return [];
258
+ if (!Array.isArray(id)) throw new TypeError(`client.action("${action.name}"): rows-level actions require an array of identifier objects; received ${describeShape(id)}.`);
259
+ return id;
260
+ }
261
+ if (id !== null && typeof id === "object" && !Array.isArray(id)) return id;
262
+ throw new TypeError(`client.action("${action.name}"): row-level actions require an identifier object; received ${describeShape(id)}.`);
218
263
  }
219
- _interpolateNavigateUrl(action, pk) {
264
+ _interpolateNavigateUrl(action, id, preferredId) {
220
265
  if (action.level !== "row") return action.value;
221
- if (pk === void 0) return action.value;
222
- return action.value.replace(/\$1/g, encodeNavigatePk(pk));
266
+ if (id === void 0) return action.value;
267
+ if (id === null || typeof id !== "object" || Array.isArray(id)) throw new TypeError(`client.action("${action.name}"): row-level navigate actions require an identifier object; received ${describeShape(id)}.`);
268
+ return action.value.replace(/\$1/g, encodeNavigateId(id, preferredId));
223
269
  }
224
270
  async _dispatchNavigate(action, url) {
225
271
  if (this._navigate) {
@@ -290,6 +336,7 @@ var Client = class {
290
336
  statusCode: res.status
291
337
  };
292
338
  }
339
+ if (errorBody.name === "ActionDisabledError") throw new ActionDisabledError(res.status, errorBody);
293
340
  throw new ClientError(res.status, errorBody);
294
341
  }
295
342
  if (!allowEmpty) return res.json();
@@ -302,14 +349,18 @@ var Client = class {
302
349
  }
303
350
  };
304
351
  /**
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).
352
+ * Encode a row identifier for substitution into a `processor: 'navigate'` URL template.
353
+ * The values are URL-encoded and joined with `/` in `meta.preferredId` field
354
+ * declaration order NOT in object-key insertion order (which is unstable
355
+ * across callers).
309
356
  */
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("/");
357
+ function encodeNavigateId(id, preferredId) {
358
+ return preferredId.map((f) => encodeURIComponent(String(id[f]))).join("/");
359
+ }
360
+ function describeShape(value) {
361
+ if (value === null) return "null";
362
+ if (Array.isArray(value)) return "array";
363
+ return typeof value;
313
364
  }
314
365
  //#endregion
315
- export { ActionNotFoundError, ActionUnsupportedError, Client, ClientError };
366
+ export { ActionDisabledError, ActionNotFoundError, ActionUnsupportedError, Client, ClientError };
@@ -1,5 +1,5 @@
1
1
  import { AggregateQuery as AggregateQuery$1, AggregateResult as AggregateResult$1, FilterExpr, TypedWithRelation, Uniquery as Uniquery$1, UniqueryControls as UniqueryControls$1 } from "@uniqu/core";
2
- import { TDbDeleteResult as TDbDeleteResult$1, TDbInsertManyResult as TDbInsertManyResult$1, TDbInsertResult as TDbInsertResult$1, TDbUpdateResult as TDbUpdateResult$1, TFieldMeta, TMetaResponse, TRelationInfo, TSearchIndexInfo } from "@atscript/db";
2
+ import { DbResponse, TDbDeleteResult as TDbDeleteResult$1, TDbInsertManyResult as TDbInsertManyResult$1, TDbInsertResult as TDbInsertResult$1, TDbUpdateResult as TDbUpdateResult$1, TFieldMeta, TMetaResponse, TRelationInfo, TSearchIndexInfo } from "@atscript/db";
3
3
  import { TAtscriptAnnotatedType, TAtscriptTypeObject } from "@atscript/typescript/utils";
4
4
  import { DbValidationContext, ValidatorMode, ValidatorMode as ValidatorMode$1 } from "@atscript/db/validator";
5
5
 
@@ -59,6 +59,21 @@ interface ServerError {
59
59
  details?: unknown[];
60
60
  }>;
61
61
  }
62
+ /**
63
+ * Minimal brand shape every `Client<T>` generic must satisfy. All fields are
64
+ * optional — plain interfaces and `Record<string, unknown>` satisfy this
65
+ * constraint, so `new Client('/path')` (no generic) keeps working with
66
+ * `unknown` / `Record<string, unknown>` fallbacks. Atscript-generated types
67
+ * fill these brand fields and unlock per-method inference.
68
+ */
69
+ type AtscriptClientShape = {
70
+ __pk?: unknown;
71
+ __ownProps?: Record<string, unknown>;
72
+ __navProps?: Record<string, unknown>;
73
+ type?: {
74
+ __dataType?: unknown;
75
+ };
76
+ };
62
77
  /** Extract the data type from an Atscript annotated type `T`. */
63
78
  type DataOf<T> = T extends {
64
79
  type: {
@@ -77,6 +92,22 @@ type NavOf<T> = T extends {
77
92
  type IdOf<T> = T extends {
78
93
  __pk: infer PK;
79
94
  } ? PK : unknown;
95
+ /**
96
+ * Narrow a read-method response type by the literal `$with` array in the
97
+ * query, mirroring the backend's `DbResponse<Data, Nav, Q>` algebra. Nav
98
+ * properties are stripped by default and re-added only for relations the
99
+ * caller listed in `$with`. When `T` carries no nav-prop brand, `DbResponse`
100
+ * short-circuits to the data type. `$actions` is always optional — the
101
+ * server emits it only when the request set `?$actions=true`.
102
+ */
103
+ type ClientResponse<T, Q> = DbResponse<DataOf<T>, NavOf<T>, Q> & {
104
+ /**
105
+ * Server-evaluated per-row availability for `'row'` and `'rows'`-level
106
+ * actions. Each entry is the `name` of an action that is NOT disabled for
107
+ * this row.
108
+ */
109
+ $actions?: string[];
110
+ };
80
111
  //#endregion
81
112
  //#region src/validator.d.ts
82
113
  /**
@@ -125,4 +156,4 @@ declare class ClientValidationError extends Error {
125
156
  */
126
157
  declare function createClientValidator(meta: MetaResponse): ClientValidator;
127
158
  //#endregion
128
- export { TDbUpdateResult$1 as C, UniqueryControls$1 as E, TDbInsertResult$1 as S, Uniquery$1 as T, RelationInfo as _, createClientValidator as a, TDbDeleteResult$1 as b, ClientOptions as c, FilterExpr as d, IdOf as f, PageResult as g, OwnOf as h, ValidatorMode$1 as i, DataOf as l, NavOf as m, ClientValidator as n, AggregateQuery$1 as o, MetaResponse as p, DbValidationContext as r, AggregateResult$1 as s, ClientValidationError as t, FieldMeta as u, SearchIndexInfo as v, TypedWithRelation as w, TDbInsertManyResult$1 as x, ServerError as y };
159
+ export { TDbInsertManyResult$1 as C, Uniquery$1 as D, TypedWithRelation as E, UniqueryControls$1 as O, TDbDeleteResult$1 as S, TDbUpdateResult$1 as T, OwnOf as _, createClientValidator as a, SearchIndexInfo as b, AtscriptClientShape as c, DataOf as d, FieldMeta as f, NavOf as g, MetaResponse as h, ValidatorMode$1 as i, ClientOptions as l, IdOf as m, ClientValidator as n, AggregateQuery$1 as o, FilterExpr as p, DbValidationContext as r, AggregateResult$1 as s, ClientValidationError as t, ClientResponse as u, PageResult as v, TDbInsertResult$1 as w, ServerError as x, RelationInfo as y };
@@ -1,7 +1,7 @@
1
1
  import { TAtscriptAnnotatedType, TAtscriptTypeObject } from "@atscript/typescript/utils";
2
2
  import { DbValidationContext, ValidatorMode, ValidatorMode as ValidatorMode$1 } from "@atscript/db/validator";
3
3
  import { AggregateQuery as AggregateQuery$1, AggregateResult as AggregateResult$1, FilterExpr, TypedWithRelation, 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, TFieldMeta, TMetaResponse, TRelationInfo, TSearchIndexInfo } from "@atscript/db";
4
+ import { DbResponse, TDbDeleteResult as TDbDeleteResult$1, TDbInsertManyResult as TDbInsertManyResult$1, TDbInsertResult as TDbInsertResult$1, TDbUpdateResult as TDbUpdateResult$1, TFieldMeta, TMetaResponse, TRelationInfo, TSearchIndexInfo } from "@atscript/db";
5
5
 
6
6
  //#region src/types.d.ts
7
7
  /** Options for creating a Client instance. */
@@ -59,6 +59,21 @@ interface ServerError {
59
59
  details?: unknown[];
60
60
  }>;
61
61
  }
62
+ /**
63
+ * Minimal brand shape every `Client<T>` generic must satisfy. All fields are
64
+ * optional — plain interfaces and `Record<string, unknown>` satisfy this
65
+ * constraint, so `new Client('/path')` (no generic) keeps working with
66
+ * `unknown` / `Record<string, unknown>` fallbacks. Atscript-generated types
67
+ * fill these brand fields and unlock per-method inference.
68
+ */
69
+ type AtscriptClientShape = {
70
+ __pk?: unknown;
71
+ __ownProps?: Record<string, unknown>;
72
+ __navProps?: Record<string, unknown>;
73
+ type?: {
74
+ __dataType?: unknown;
75
+ };
76
+ };
62
77
  /** Extract the data type from an Atscript annotated type `T`. */
63
78
  type DataOf<T> = T extends {
64
79
  type: {
@@ -77,6 +92,22 @@ type NavOf<T> = T extends {
77
92
  type IdOf<T> = T extends {
78
93
  __pk: infer PK;
79
94
  } ? PK : unknown;
95
+ /**
96
+ * Narrow a read-method response type by the literal `$with` array in the
97
+ * query, mirroring the backend's `DbResponse<Data, Nav, Q>` algebra. Nav
98
+ * properties are stripped by default and re-added only for relations the
99
+ * caller listed in `$with`. When `T` carries no nav-prop brand, `DbResponse`
100
+ * short-circuits to the data type. `$actions` is always optional — the
101
+ * server emits it only when the request set `?$actions=true`.
102
+ */
103
+ type ClientResponse<T, Q> = DbResponse<DataOf<T>, NavOf<T>, Q> & {
104
+ /**
105
+ * Server-evaluated per-row availability for `'row'` and `'rows'`-level
106
+ * actions. Each entry is the `name` of an action that is NOT disabled for
107
+ * this row.
108
+ */
109
+ $actions?: string[];
110
+ };
80
111
  //#endregion
81
112
  //#region src/validator.d.ts
82
113
  /**
@@ -125,4 +156,4 @@ declare class ClientValidationError extends Error {
125
156
  */
126
157
  declare function createClientValidator(meta: MetaResponse): ClientValidator;
127
158
  //#endregion
128
- export { TDbUpdateResult$1 as C, UniqueryControls$1 as E, TDbInsertResult$1 as S, Uniquery$1 as T, RelationInfo as _, createClientValidator as a, TDbDeleteResult$1 as b, ClientOptions as c, FilterExpr as d, IdOf as f, PageResult as g, OwnOf as h, ValidatorMode$1 as i, DataOf as l, NavOf as m, ClientValidator as n, AggregateQuery$1 as o, MetaResponse as p, DbValidationContext as r, AggregateResult$1 as s, ClientValidationError as t, FieldMeta as u, SearchIndexInfo as v, TypedWithRelation as w, TDbInsertManyResult$1 as x, ServerError as y };
159
+ export { TDbInsertManyResult$1 as C, Uniquery$1 as D, TypedWithRelation as E, UniqueryControls$1 as O, TDbDeleteResult$1 as S, TDbUpdateResult$1 as T, OwnOf as _, createClientValidator as a, SearchIndexInfo as b, AtscriptClientShape as c, DataOf as d, FieldMeta as f, NavOf as g, MetaResponse as h, ValidatorMode$1 as i, ClientOptions as l, IdOf as m, ClientValidator as n, AggregateQuery$1 as o, FilterExpr as p, DbValidationContext as r, AggregateResult$1 as s, ClientValidationError as t, ClientResponse as u, PageResult as v, TDbInsertResult$1 as w, ServerError as x, RelationInfo as y };
@@ -1,2 +1,2 @@
1
- import { a as createClientValidator, i as ValidatorMode, n as ClientValidator, r as DbValidationContext, t as ClientValidationError } from "./validator-DNm9kCoq.cjs";
1
+ import { a as createClientValidator, i as ValidatorMode, n as ClientValidator, r as DbValidationContext, t as ClientValidationError } from "./validator-BminFmAf.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-DfNMCEKa.mjs";
1
+ import { a as createClientValidator, i as ValidatorMode, n as ClientValidator, r as DbValidationContext, t as ClientValidationError } from "./validator-DkIQ_0Lc.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.57",
3
+ "version": "0.1.59",
4
4
  "description": "Browser-compatible HTTP client for @atscript/moost-db REST endpoints.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -50,7 +50,7 @@
50
50
  "@atscript/typescript": "^0.1.50",
51
51
  "@uniqu/core": "^0.1.5",
52
52
  "unplugin-atscript": "^0.1.50",
53
- "@atscript/db": "0.1.57"
53
+ "@atscript/db": "0.1.59"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "@atscript/db": "^0.1.44",