@atscript/db-client 0.1.146 → 0.1.148

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
@@ -65,6 +65,29 @@ var ActionDisabledError = class extends ClientError {
65
65
  }
66
66
  };
67
67
  /**
68
+ * Typed marker thrown by `Client` when a query-targeted action request is
69
+ * refused (body `name === 'ActionTargetError'`): `TARGET_INVALID` (400),
70
+ * `TARGET_TOO_LARGE` (400, `cap`) or `TARGET_CHANGED` (409, `matched` — the
71
+ * rows the query matches now; re-confirm and retry with that count).
72
+ *
73
+ * @since 0.1.147
74
+ */
75
+ var ActionTargetError = class extends ClientError {
76
+ name = "ActionTargetError";
77
+ get code() {
78
+ return this.body.code;
79
+ }
80
+ get action() {
81
+ return this.body.action;
82
+ }
83
+ get matched() {
84
+ return this.body.matched;
85
+ }
86
+ get cap() {
87
+ return this.body.cap;
88
+ }
89
+ };
90
+ /**
68
91
  * Typed marker thrown by `Client._send` when the server response body's
69
92
  * `kind === 'version_mismatch'`. The transport / status / base body are
70
93
  * identical to a generic `ClientError`; this subclass adds a typed
@@ -143,6 +166,17 @@ var ActionUnsupportedError = class extends Error {
143
166
  * const all = await users.query()
144
167
  * const page = await users.pages({ filter: { active: true } }, 1, 20)
145
168
  * ```
169
+ *
170
+ * `D` (since 0.1.148) types the display-only fields the controller declares
171
+ * with `@DbDecorations`: `$select` of the read methods accepts their keys and
172
+ * rows carry them as optional properties. Filter and sort stay over `T`'s own
173
+ * fields — a decoration is never filterable or sortable.
174
+ *
175
+ * ```typescript
176
+ * const tickets = new Client<typeof Ticket, TicketDecorations>('/api/tickets')
177
+ * const rows = await tickets.query({ controls: { $select: ['title', 'ownerName'] } })
178
+ * rows[0].ownerName // string | undefined
179
+ * ```
146
180
  */
147
181
  var Client = class {
148
182
  _path;
@@ -186,8 +220,15 @@ var Client = class {
186
220
  *
187
221
  * `$select` may carry calendar buckets (`{ $bucket, $field, $tz?, $weekStart?, $as? }`);
188
222
  * a `$groupBy` entry must be a dimension or a bucket alias (`ValidGroupBy`), and a
189
- * bucket's value is typed as its `YYYY-MM-DD` label (`| null` for an optional source).
223
+ * bucket's value is typed as its `YYYY-MM-DD` (hour: `YYYY-MM-DDTHH:00`) label
224
+ * (`| null` for an optional source).
190
225
  * Gap-fill between labels with `nextBucketLabel` (re-exported here).
226
+ *
227
+ * Arithmetic (since 0.1.148): `{ $fn: 'sum', $expr, $as }` aggregates a per-row
228
+ * expression over numeric fields; `{ $expr, $as }` computes over aliases of other
229
+ * numeric entries or numeric `$groupBy` fields (`number | null`). `first` / `last`
230
+ * read a representative row ordered by `$rowOrder`. The URL forms are `expr(a/b):x`,
231
+ * `sum(a*b):x`, `first(f):x` and `$rowOrder=f,-id`.
191
232
  */
192
233
  async aggregate(query) {
193
234
  return this._get("query", query);
@@ -250,8 +291,9 @@ var Client = class {
250
291
  const controlStr = query?.controls ? (0, _uniqu_url_builder.buildUrl)({ controls: query.controls }) : "";
251
292
  return this._getOrNull(this._idUrl("one", id, controlStr));
252
293
  }
253
- async insert(data) {
254
- return this._request("POST", "", await this._prepareWrite(data, "insert"));
294
+ async insert(data, opts) {
295
+ const query = opts?.onConflict === "ignore" ? "?$onConflict=ignore" : "";
296
+ return this._request("POST", query, await this._prepareWrite(data, "insert"));
255
297
  }
256
298
  /**
257
299
  * `PATCH /` — partial update one or many records by primary key.
@@ -323,6 +365,13 @@ var Client = class {
323
365
  * `{ ids?, input? }` — `ids` carries `id` (object or array per level),
324
366
  * `input` carries the form payload.
325
367
  *
368
+ * **Delegated actions** (since 0.1.147). An action another controller owns
369
+ * (`owner`, e.g. a view listing its source table's actions) may carry
370
+ * `idMap`: each `id` is then a row (or identifier) of THIS controller and
371
+ * is mapped to the owner's identification — `{ [ownerField]:
372
+ * row[path] }` — before it is sent to `value` (the owner's route). A
373
+ * missing path throws `TypeError`. See {@link actionIdentifier}.
374
+ *
326
375
  * @typeParam R Caller-asserted return shape from the action handler. The
327
376
  * server returns whatever the handler emits (commonly
328
377
  * `{ message?: string, ... }`); the client cannot validate.
@@ -332,15 +381,65 @@ var Client = class {
332
381
  const action = meta.actions.find((a) => a.name === name);
333
382
  if (!action) throw new ActionNotFoundError(name);
334
383
  if (action.processor === "custom") throw new ActionUnsupportedError(name, "custom", `Action "${name}" has processor "custom" — applications must dispatch custom actions themselves; the client cannot.`);
384
+ const mapped = action.idMap ? mapDelegatedIds(action, id) : id;
335
385
  if (action.processor === "navigate") {
336
- const url = this._interpolateNavigateUrl(action, id, meta.preferredId);
386
+ const order = action.idMap ? Object.keys(action.idMap) : meta.preferredId;
387
+ const url = this._interpolateNavigateUrl(action, mapped, order);
337
388
  await this._dispatchNavigate(action, url);
338
389
  return;
339
390
  }
340
- const body = this._buildActionBody(action, id, input);
391
+ const body = this._buildActionBody(action, mapped, input);
341
392
  return this._postAction(action, body);
342
393
  }
343
394
  /**
395
+ * Runs a `'rows'` action on every row matching a query (since 0.1.147) —
396
+ * the action's `/meta` entry must carry `queryTarget`. POSTs
397
+ * `{ query: { q, exclude?, expectCount?, maxRows? }, input? }` to
398
+ * `queryTarget.url` (a delegated action: the view resolves the rows and
399
+ * runs the owner's action in batches) or `value`. `q` is the `/query`
400
+ * string of `target.filter` / `search` / `index`.
401
+ *
402
+ * The server answers what the handler returns — for a delegated action
403
+ * (and a handler returning `target.summary()`) a
404
+ * {@link TDbActionTargetSummary}. Refusals arrive as
405
+ * {@link ActionTargetError}: `TARGET_TOO_LARGE` (`cap`), `TARGET_CHANGED`
406
+ * (`matched` differs from `expectCount`), `TARGET_INVALID`. Throws
407
+ * {@link ActionUnsupportedError} when the action takes no query target.
408
+ *
409
+ * @since 0.1.147
410
+ */
411
+ async actionOnQuery(name, target, input) {
412
+ const action = await this._queryTargetAction(name);
413
+ const body = { query: queryTargetBody(target) };
414
+ if (input !== void 0) body.input = input;
415
+ return this._postQueryTarget(action, body);
416
+ }
417
+ /**
418
+ * How many rows {@link actionOnQuery} would target right now (a dry run:
419
+ * the handler does not run) — pass the answer as `expectCount` to make the
420
+ * real run fail with `TARGET_CHANGED` if the set changed in between.
421
+ *
422
+ * @since 0.1.147
423
+ */
424
+ async countActionTarget(name, target) {
425
+ const action = await this._queryTargetAction(name);
426
+ const query = {
427
+ ...queryTargetBody(target),
428
+ dryRun: true
429
+ };
430
+ return this._postQueryTarget(action, { query });
431
+ }
432
+ async _queryTargetAction(name) {
433
+ const action = (await this.meta()).actions.find((a) => a.name === name);
434
+ if (!action) throw new ActionNotFoundError(name);
435
+ if (!action.queryTarget || action.processor !== "backend") throw new ActionUnsupportedError(name, action.processor, `Action "${name}" does not accept a query target (no \`queryTarget\` in /meta).`);
436
+ return action;
437
+ }
438
+ _postQueryTarget(action, body) {
439
+ const path = action.queryTarget?.url ?? action.value;
440
+ return this._requestUrl("POST", `${this._baseUrl}${path}`, body, true);
441
+ }
442
+ /**
344
443
  * `GET /meta/actions/:id` or `GET /meta/actions?k1=v1&k2=v2` — the
345
444
  * row-level actions the caller may run on one row right now, and the
346
445
  * reasons of those disabled with one: `{ actions, disabledReasons? }`.
@@ -519,6 +618,7 @@ var Client = class {
519
618
  };
520
619
  }
521
620
  if (errorBody.name === "ActionDisabledError") throw new ActionDisabledError(res.status, errorBody);
621
+ if (errorBody.name === "ActionTargetError") throw new ActionTargetError(res.status, errorBody);
522
622
  if (errorBody.kind === "version_mismatch") throw new VersionMismatchError(res.status, errorBody);
523
623
  throw new ClientError(res.status, errorBody);
524
624
  }
@@ -540,6 +640,60 @@ function describeCause(cause) {
540
640
  if (cause instanceof Error) return cause.message || cause.name;
541
641
  return String(cause);
542
642
  }
643
+ /** The value at a dot `path` of `row`. */
644
+ function valueAt(row, path) {
645
+ if (!path.includes(".") || Object.hasOwn(row, path)) return row[path];
646
+ let v = row;
647
+ for (const part of path.split(".")) v = v?.[part];
648
+ return v;
649
+ }
650
+ /**
651
+ * The identifier `action` takes for one row (or identifier) of the
652
+ * controller whose `/meta` listed it (since 0.1.147):
653
+ *
654
+ * - a delegated action with `idMap` → the owner's identification,
655
+ * `{ [ownerField]: rowOrId[path] }` (dot paths allowed); a missing value
656
+ * throws `TypeError` naming the action and the path;
657
+ * - any other action → `rowOrId`'s `preferredId` fields when it carries them
658
+ * all, else `rowOrId` itself (already an identifier).
659
+ *
660
+ * UIs build `ids` from loaded rows with it; `Client.action()` applies the
661
+ * `idMap` mapping itself.
662
+ *
663
+ * @since 0.1.147
664
+ */
665
+ function actionIdentifier(action, rowOrId, preferredId) {
666
+ if (action.idMap) {
667
+ const out = {};
668
+ for (const [field, path] of Object.entries(action.idMap)) {
669
+ const value = valueAt(rowOrId, path);
670
+ if (value === void 0 || value === null) throw new TypeError(`client.action("${action.name}"): the identifier has no "${path}" — needed for the owner's "${field}".`);
671
+ out[field] = value;
672
+ }
673
+ return out;
674
+ }
675
+ if (preferredId.length > 0 && preferredId.every((f) => rowOrId[f] !== void 0)) return Object.fromEntries(preferredId.map((f) => [f, rowOrId[f]]));
676
+ return rowOrId;
677
+ }
678
+ /** `action()`'s `id` argument through a delegated action's `idMap` (shape errors are left to the body builder). */
679
+ function mapDelegatedIds(action, id) {
680
+ const map = (one) => one !== null && typeof one === "object" && !Array.isArray(one) ? actionIdentifier(action, one, []) : one;
681
+ return Array.isArray(id) ? id.map(map) : map(id);
682
+ }
683
+ /** The wire `query` of a {@link TDbQueryTarget}. */
684
+ function queryTargetBody(target) {
685
+ const controls = {};
686
+ if (target.search !== void 0) controls.$search = target.search;
687
+ if (target.index !== void 0) controls.$index = target.index;
688
+ const out = { q: (0, _uniqu_url_builder.buildUrl)({
689
+ filter: target.filter ?? {},
690
+ controls
691
+ }) };
692
+ if (target.exclude?.length) out.exclude = target.exclude;
693
+ if (target.expectCount !== void 0) out.expectCount = target.expectCount;
694
+ if (target.maxRows !== void 0) out.maxRows = target.maxRows;
695
+ return out;
696
+ }
543
697
  /**
544
698
  * Render a single identifier field for substitution into a navigate-URL
545
699
  * template or human-readable string. `null` / `undefined` collapse to `""`
@@ -578,11 +732,19 @@ function describeShape(value) {
578
732
  //#endregion
579
733
  exports.ActionDisabledError = ActionDisabledError;
580
734
  exports.ActionNotFoundError = ActionNotFoundError;
735
+ exports.ActionTargetError = ActionTargetError;
581
736
  exports.ActionUnsupportedError = ActionUnsupportedError;
582
737
  exports.Client = Client;
583
738
  exports.ClientError = ClientError;
584
739
  exports.TransportError = TransportError;
585
740
  exports.VersionMismatchError = VersionMismatchError;
741
+ exports.actionIdentifier = actionIdentifier;
742
+ Object.defineProperty(exports, "bucketSeries", {
743
+ enumerable: true,
744
+ get: function() {
745
+ return _uniqu_core.bucketSeries;
746
+ }
747
+ });
586
748
  Object.defineProperty(exports, "bucketStartInstant", {
587
749
  enumerable: true,
588
750
  get: function() {
package/dist/index.d.cts CHANGED
@@ -1,13 +1,15 @@
1
- import { A as TDbDeleteResult, C as OwnOf, D as RowOf, E as RelationInfo, F as Uniquery, I as UniqueryControls, L as ValidGroupBy, M as TDbInsertResult, N as TDbUpdateResult, O as SearchIndexInfo, P as TypedWithRelation, R as WeekStart, S as NavOf, T as PatchOf, _ as DbRow, a as ValidatorMode, b as IdOf, c as AggregateResult, d as BucketUnit, f as CalendarBucketLabel, g as DbPatch, h as DataOf, j as TDbInsertManyResult, k as ServerError, l as AtscriptClientShape, m as ClientResponse, n as ClientValidator, p as ClientOptions, r as ClientValidatorOptions, s as AggregateQuery, t as ClientValidationError, u as BucketExpr, v as FieldMeta, w as PageResult, x as MetaResponse, y as FilterExpr } from "./validator-810-rhnQ.cjs";
2
- import { AggregateExpr, AggregateQuery as AggregateQuery$1, AggregateResult as AggregateResult$1, BucketExpr as BucketExpr$1, Uniquery as Uniquery$1, UniqueryControls as UniqueryControls$1, ValidGroupBy as ValidGroupBy$1, bucketStartInstant, nextBucketLabel } from "@uniqu/core";
1
+ import { A as SearchIndexInfo, B as Uniquery, C as MetaResponse, D as PatchOf, E as PageResult, F as TDbInsertManyResult, H as ValidGroupBy, I as TDbInsertResult, L as TDbQueryTarget, M as TDbDeleteResult, N as TDbInsertIgnoreResult, O as RelationInfo, P as TDbInsertManyIgnoreResult, R as TDbUpdateResult, S as IdOf, T as OwnOf, U as WeekStart, V as UniqueryControls, _ as DbRow, a as ValidatorMode, b as FieldMeta, c as AggregateResult, d as BucketUnit, f as CalendarBucketLabel, g as DbPatch, h as DataOf, j as ServerError, k as RowOf, l as AtscriptClientShape, m as ClientResponse, n as ClientValidator, p as ClientOptions, r as ClientValidatorOptions, s as AggregateQuery, t as ClientValidationError, u as BucketExpr, v as DecoratedControls, w as NavOf, x as FilterExpr, y as DecoratedQuery, z as TypedWithRelation } from "./validator-CVM7eNP7.cjs";
2
+ import { AggregateExpr, AggregateOfExpr, AggregateQuery as AggregateQuery$1, AggregateResult as AggregateResult$1, BucketExpr as BucketExpr$1, BucketSeriesOptions, NextBucketOptions, SelectArithExpr, Uniquery as Uniquery$1, ValidGroupBy as ValidGroupBy$1, bucketSeries, bucketStartInstant, nextBucketLabel } from "@uniqu/core";
3
3
  import { TAtscriptAnnotatedType, TSerializedAnnotatedType } from "@atscript/typescript/utils";
4
- import { TCrudOp, TCrudPermissions, TDbActionInfo, TDbActionIntent, TDbActionLevel, TDbActionProcessor, TDbAvailableActions, TDbAvailableActions as TDbAvailableActions$1, TDbDeleteResult as TDbDeleteResult$1, TDbInsertManyResult as TDbInsertManyResult$1, TDbInsertResult as TDbInsertResult$1, TDbUpdateResult as TDbUpdateResult$1 } from "@atscript/db";
4
+ import { TCrudOp, TCrudPermissions, TDbActionInfo, TDbActionInfo as TDbActionInfo$1, TDbActionIntent, TDbActionLevel, TDbActionProcessor, TDbActionTargetSummary, TDbActionTargetSummary as TDbActionTargetSummary$1, TDbAvailableActions, TDbAvailableActions as TDbAvailableActions$1, TDbDeleteResult as TDbDeleteResult$1, TDbInsertIgnoreResult as TDbInsertIgnoreResult$1, TDbInsertManyIgnoreResult as TDbInsertManyIgnoreResult$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>;
8
8
  type Nav<T> = NavOf<T>;
9
9
  type Id<T> = IdOf<T>;
10
10
  type Response<T, Q> = ClientResponse<T, Q>;
11
+ /** A read row: the response over the own fields plus the declared decorations `D`, each optional. */
12
+ type DecoratedRow<T, Q, D> = Response<T, Q> & Partial<D>;
11
13
  /**
12
14
  * HTTP client for moost-db REST endpoints.
13
15
  *
@@ -28,8 +30,19 @@ type Response<T, Q> = ClientResponse<T, Q>;
28
30
  * const all = await users.query()
29
31
  * const page = await users.pages({ filter: { active: true } }, 1, 20)
30
32
  * ```
33
+ *
34
+ * `D` (since 0.1.148) types the display-only fields the controller declares
35
+ * with `@DbDecorations`: `$select` of the read methods accepts their keys and
36
+ * rows carry them as optional properties. Filter and sort stay over `T`'s own
37
+ * fields — a decoration is never filterable or sortable.
38
+ *
39
+ * ```typescript
40
+ * const tickets = new Client<typeof Ticket, TicketDecorations>('/api/tickets')
41
+ * const rows = await tickets.query({ controls: { $select: ['title', 'ownerName'] } })
42
+ * rows[0].ownerName // string | undefined
43
+ * ```
31
44
  */
32
- declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
45
+ declare class Client<T extends AtscriptClientShape = AtscriptClientShape, D extends object = Record<never, never>> {
33
46
  private readonly _path;
34
47
  private readonly _baseUrl;
35
48
  private readonly _fetch;
@@ -47,7 +60,7 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
47
60
  * The response type narrows by the literal `$with` array in `query` —
48
61
  * relations not listed in `$with` are stripped from the row type.
49
62
  */
50
- query<Q extends Uniquery$1<Own<T>, Nav<T>> = Uniquery$1<Own<T>, Nav<T>>>(query?: Q): Promise<Response<T, Q>[]>;
63
+ query<Q extends DecoratedQuery<T, D> = DecoratedQuery<T, D>>(query?: Q): Promise<DecoratedRow<T, Q, D>[]>;
51
64
  /**
52
65
  * `GET /query` with `$count: true` — returns record count.
53
66
  */
@@ -59,17 +72,24 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
59
72
  *
60
73
  * `$select` may carry calendar buckets (`{ $bucket, $field, $tz?, $weekStart?, $as? }`);
61
74
  * a `$groupBy` entry must be a dimension or a bucket alias (`ValidGroupBy`), and a
62
- * bucket's value is typed as its `YYYY-MM-DD` label (`| null` for an optional source).
75
+ * bucket's value is typed as its `YYYY-MM-DD` (hour: `YYYY-MM-DDTHH:00`) label
76
+ * (`| null` for an optional source).
63
77
  * Gap-fill between labels with `nextBucketLabel` (re-exported here).
78
+ *
79
+ * Arithmetic (since 0.1.148): `{ $fn: 'sum', $expr, $as }` aggregates a per-row
80
+ * expression over numeric fields; `{ $expr, $as }` computes over aliases of other
81
+ * numeric entries or numeric `$groupBy` fields (`number | null`). `first` / `last`
82
+ * read a representative row ordered by `$rowOrder`. The URL forms are `expr(a/b):x`,
83
+ * `sum(a*b):x`, `first(f):x` and `$rowOrder=f,-id`.
64
84
  */
65
- aggregate<const Q extends AggregateQuery$1<Own<T>>>(query: Q & ValidGroupBy$1<Own<T>, Q>): Promise<Q["controls"]["$select"] extends readonly (string | AggregateExpr | BucketExpr$1)[] ? AggregateResult$1<Own<T>, Q["controls"]["$select"]>[] : Record<string, unknown>[]>;
85
+ aggregate<const Q extends AggregateQuery$1<Own<T>>>(query: Q & ValidGroupBy$1<Own<T>, Q>): Promise<Q["controls"]["$select"] extends readonly (string | AggregateExpr | BucketExpr$1 | AggregateOfExpr | SelectArithExpr)[] ? AggregateResult$1<Own<T>, Q["controls"]["$select"]>[] : Record<string, unknown>[]>;
66
86
  /**
67
87
  * `GET /pages` — paginated query with typed filter and relations.
68
88
  *
69
89
  * Response rows narrow by the literal `$with` array — same algebra as
70
90
  * {@link query}.
71
91
  */
72
- 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>>>;
92
+ pages<Q extends DecoratedQuery<T, D> = DecoratedQuery<T, D>>(query?: Q, page?: number, size?: number): Promise<PageResult<DecoratedRow<T, Q, D>>>;
73
93
  /**
74
94
  * `GET /geo` — distance-ranked geospatial search (tables with `@db.index.geo`).
75
95
  *
@@ -79,13 +99,13 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
79
99
  * in `query.controls`; filter / `$select` / `$with` / `$skip` / `$limit`
80
100
  * compose as usual.
81
101
  */
82
- geoSearch<Q extends Uniquery$1<Own<T>, Nav<T>> = Uniquery$1<Own<T>, Nav<T>>>(point: [number, number], query?: Q): Promise<Array<Response<T, Q> & {
102
+ geoSearch<Q extends DecoratedQuery<T, D> = DecoratedQuery<T, D>>(point: [number, number], query?: Q): Promise<Array<DecoratedRow<T, Q, D> & {
83
103
  $distance: number;
84
104
  }>>;
85
105
  /**
86
106
  * `GET /geo` with `$page` / `$size` — paginated distance-ranked search.
87
107
  */
88
- geoPages<Q extends Uniquery$1<Own<T>, Nav<T>> = Uniquery$1<Own<T>, Nav<T>>>(point: [number, number], query?: Q, page?: number, size?: number): Promise<PageResult<Response<T, Q> & {
108
+ geoPages<Q extends DecoratedQuery<T, D> = DecoratedQuery<T, D>>(point: [number, number], query?: Q, page?: number, size?: number): Promise<PageResult<DecoratedRow<T, Q, D> & {
89
109
  $distance: number;
90
110
  }>>;
91
111
  /**
@@ -95,10 +115,10 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
95
115
  * `query.controls` — same algebra as {@link query}.
96
116
  */
97
117
  one<Q extends {
98
- controls?: UniqueryControls$1<Own<T>, Nav<T>>;
118
+ controls?: DecoratedControls<T, D>;
99
119
  } = {
100
- controls?: UniqueryControls$1<Own<T>, Nav<T>>;
101
- }>(id: Id<T>, query?: Q): Promise<Response<T, Q> | null>;
120
+ controls?: DecoratedControls<T, D>;
121
+ }>(id: Id<T>, query?: Q): Promise<DecoratedRow<T, Q, D> | null>;
102
122
  /**
103
123
  * `POST /` — insert one record. The version column (if any) is optional —
104
124
  * the server initialises it; `$cas` is rejected (no meaning on insert).
@@ -108,6 +128,20 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
108
128
  * `POST /` — insert many records.
109
129
  */
110
130
  insert(data: PatchOf<T>[]): Promise<TDbInsertManyResult$1>;
131
+ /**
132
+ * `POST /?$onConflict=ignore` — insert one record, skipping it when it
133
+ * collides on the primary key or a unique index (since 0.1.148).
134
+ */
135
+ insert(data: PatchOf<T>, opts: {
136
+ onConflict: "ignore";
137
+ }): Promise<TDbInsertIgnoreResult$1>;
138
+ /**
139
+ * `POST /?$onConflict=ignore` — insert many records, skipping the ones that
140
+ * collide; the result reports inserted and skipped input indices (since 0.1.148).
141
+ */
142
+ insert(data: PatchOf<T>[], opts: {
143
+ onConflict: "ignore";
144
+ }): Promise<TDbInsertManyIgnoreResult$1>;
111
145
  /**
112
146
  * `PATCH /` — partial update one or many records by primary key.
113
147
  *
@@ -166,11 +200,48 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
166
200
  * `{ ids?, input? }` — `ids` carries `id` (object or array per level),
167
201
  * `input` carries the form payload.
168
202
  *
203
+ * **Delegated actions** (since 0.1.147). An action another controller owns
204
+ * (`owner`, e.g. a view listing its source table's actions) may carry
205
+ * `idMap`: each `id` is then a row (or identifier) of THIS controller and
206
+ * is mapped to the owner's identification — `{ [ownerField]:
207
+ * row[path] }` — before it is sent to `value` (the owner's route). A
208
+ * missing path throws `TypeError`. See {@link actionIdentifier}.
209
+ *
169
210
  * @typeParam R Caller-asserted return shape from the action handler. The
170
211
  * server returns whatever the handler emits (commonly
171
212
  * `{ message?: string, ... }`); the client cannot validate.
172
213
  */
173
214
  action<R = unknown>(name: string, id?: Partial<Own<T>> | Partial<Own<T>>[], input?: unknown): Promise<R>;
215
+ /**
216
+ * Runs a `'rows'` action on every row matching a query (since 0.1.147) —
217
+ * the action's `/meta` entry must carry `queryTarget`. POSTs
218
+ * `{ query: { q, exclude?, expectCount?, maxRows? }, input? }` to
219
+ * `queryTarget.url` (a delegated action: the view resolves the rows and
220
+ * runs the owner's action in batches) or `value`. `q` is the `/query`
221
+ * string of `target.filter` / `search` / `index`.
222
+ *
223
+ * The server answers what the handler returns — for a delegated action
224
+ * (and a handler returning `target.summary()`) a
225
+ * {@link TDbActionTargetSummary}. Refusals arrive as
226
+ * {@link ActionTargetError}: `TARGET_TOO_LARGE` (`cap`), `TARGET_CHANGED`
227
+ * (`matched` differs from `expectCount`), `TARGET_INVALID`. Throws
228
+ * {@link ActionUnsupportedError} when the action takes no query target.
229
+ *
230
+ * @since 0.1.147
231
+ */
232
+ actionOnQuery<R = TDbActionTargetSummary$1>(name: string, target: TDbQueryTarget<T>, input?: unknown): Promise<R>;
233
+ /**
234
+ * How many rows {@link actionOnQuery} would target right now (a dry run:
235
+ * the handler does not run) — pass the answer as `expectCount` to make the
236
+ * real run fail with `TARGET_CHANGED` if the set changed in between.
237
+ *
238
+ * @since 0.1.147
239
+ */
240
+ countActionTarget(name: string, target: TDbQueryTarget<T>): Promise<{
241
+ matched: number;
242
+ }>;
243
+ private _queryTargetAction;
244
+ private _postQueryTarget;
174
245
  /**
175
246
  * `GET /meta/actions/:id` or `GET /meta/actions?k1=v1&k2=v2` — the
176
247
  * row-level actions the caller may run on one row right now, and the
@@ -224,6 +295,22 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
224
295
  private _requestUrl;
225
296
  private _send;
226
297
  }
298
+ /**
299
+ * The identifier `action` takes for one row (or identifier) of the
300
+ * controller whose `/meta` listed it (since 0.1.147):
301
+ *
302
+ * - a delegated action with `idMap` → the owner's identification,
303
+ * `{ [ownerField]: rowOrId[path] }` (dot paths allowed); a missing value
304
+ * throws `TypeError` naming the action and the path;
305
+ * - any other action → `rowOrId`'s `preferredId` fields when it carries them
306
+ * all, else `rowOrId` itself (already an identifier).
307
+ *
308
+ * UIs build `ids` from loaded rows with it; `Client.action()` applies the
309
+ * `idMap` mapping itself.
310
+ *
311
+ * @since 0.1.147
312
+ */
313
+ declare function actionIdentifier(action: Pick<TDbActionInfo$1, "name" | "idMap">, rowOrId: Record<string, unknown>, preferredId: readonly string[]): Record<string, unknown>;
227
314
  /**
228
315
  * Render a single identifier field for substitution into a navigate-URL
229
316
  * template or human-readable string. `null` / `undefined` collapse to `""`
@@ -318,6 +405,34 @@ declare class ActionDisabledError extends ClientError {
318
405
  */
319
406
  get reasons(): (string | null)[] | undefined;
320
407
  }
408
+ /**
409
+ * Wire-body shape for `ActionTargetError` responses (since 0.1.147): a
410
+ * query target the server refused — `code` says why.
411
+ */
412
+ interface ActionTargetErrorBody extends ServerError {
413
+ name: "ActionTargetError";
414
+ code: "TARGET_INVALID" | "TARGET_TOO_LARGE" | "TARGET_CHANGED";
415
+ action: string;
416
+ /** `TARGET_CHANGED`: the current match count. */
417
+ matched?: number;
418
+ /** `TARGET_TOO_LARGE`: the most rows one request may target. */
419
+ cap?: number;
420
+ }
421
+ /**
422
+ * Typed marker thrown by `Client` when a query-targeted action request is
423
+ * refused (body `name === 'ActionTargetError'`): `TARGET_INVALID` (400),
424
+ * `TARGET_TOO_LARGE` (400, `cap`) or `TARGET_CHANGED` (409, `matched` — the
425
+ * rows the query matches now; re-confirm and retry with that count).
426
+ *
427
+ * @since 0.1.147
428
+ */
429
+ declare class ActionTargetError extends ClientError {
430
+ name: string;
431
+ get code(): ActionTargetErrorBody["code"];
432
+ get action(): string;
433
+ get matched(): number | undefined;
434
+ get cap(): number | undefined;
435
+ }
321
436
  /**
322
437
  * Wire-body shape for 409 OCC `version_mismatch` responses. Extends the base
323
438
  * `ServerError` envelope with a `kind` discriminator and the row's current
@@ -382,4 +497,4 @@ declare class ActionUnsupportedError extends Error {
382
497
  constructor(action: string, processor: string, message: string);
383
498
  }
384
499
  //#endregion
385
- export { ActionDisabledError, type ActionDisabledErrorBody, ActionNotFoundError, ActionUnsupportedError, type AggregateQuery, type AggregateResult, type AtscriptClientShape, type BucketExpr, type BucketUnit, type CalendarBucketLabel, Client, ClientError, type ClientOptions, type ClientResponse, type ClientValidationError, type ClientValidator, type ClientValidatorOptions, type DataOf, type DbPatch, type DbRow, type FieldMeta, type FilterExpr, type IdOf, type MetaResponse, type NavOf, type OwnOf, type PageResult, type PatchOf, type RelationInfo, type RowOf, type SearchIndexInfo, type ServerError, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbAvailableActions, type TDbDeleteResult, type TDbInsertManyResult, type TDbInsertResult, type TDbUpdateResult, type TSerializedAnnotatedType, TransportError, type TypedWithRelation, type Uniquery, type UniqueryControls, type ValidGroupBy, type ValidatorMode, VersionMismatchError, type VersionMismatchErrorBody, type WeekStart, bucketStartInstant, encodeNavigateId, formatIdentifier, formatIdentifierField, nextBucketLabel };
500
+ export { ActionDisabledError, type ActionDisabledErrorBody, ActionNotFoundError, ActionTargetError, type ActionTargetErrorBody, ActionUnsupportedError, type AggregateQuery, type AggregateResult, type AtscriptClientShape, type BucketExpr, type BucketSeriesOptions, type BucketUnit, type CalendarBucketLabel, Client, ClientError, type ClientOptions, type ClientResponse, type ClientValidationError, type ClientValidator, type ClientValidatorOptions, type DataOf, type DbPatch, type DbRow, type FieldMeta, type FilterExpr, type IdOf, type MetaResponse, type NavOf, type NextBucketOptions, type OwnOf, type PageResult, type PatchOf, type RelationInfo, type RowOf, type SearchIndexInfo, type ServerError, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbActionTargetSummary, type TDbAvailableActions, type TDbDeleteResult, type TDbInsertIgnoreResult, type TDbInsertManyIgnoreResult, type TDbInsertManyResult, type TDbInsertResult, type TDbQueryTarget, type TDbUpdateResult, type TSerializedAnnotatedType, TransportError, type TypedWithRelation, type Uniquery, type UniqueryControls, type ValidGroupBy, type ValidatorMode, VersionMismatchError, type VersionMismatchErrorBody, type WeekStart, actionIdentifier, bucketSeries, bucketStartInstant, encodeNavigateId, formatIdentifier, formatIdentifierField, nextBucketLabel };
package/dist/index.d.mts CHANGED
@@ -1,13 +1,15 @@
1
- import { A as TDbDeleteResult, C as OwnOf, D as RowOf, E as RelationInfo, F as Uniquery, I as UniqueryControls, L as ValidGroupBy, M as TDbInsertResult, N as TDbUpdateResult, O as SearchIndexInfo, P as TypedWithRelation, R as WeekStart, S as NavOf, T as PatchOf, _ as DbRow, a as ValidatorMode, b as IdOf, c as AggregateResult, d as BucketUnit, f as CalendarBucketLabel, g as DbPatch, h as DataOf, j as TDbInsertManyResult, k as ServerError, l as AtscriptClientShape, m as ClientResponse, n as ClientValidator, p as ClientOptions, r as ClientValidatorOptions, s as AggregateQuery, t as ClientValidationError, u as BucketExpr, v as FieldMeta, w as PageResult, x as MetaResponse, y as FilterExpr } from "./validator-CIm1ZCS5.mjs";
1
+ import { A as SearchIndexInfo, B as Uniquery, C as MetaResponse, D as PatchOf, E as PageResult, F as TDbInsertManyResult, H as ValidGroupBy, I as TDbInsertResult, L as TDbQueryTarget, M as TDbDeleteResult, N as TDbInsertIgnoreResult, O as RelationInfo, P as TDbInsertManyIgnoreResult, R as TDbUpdateResult, S as IdOf, T as OwnOf, U as WeekStart, V as UniqueryControls, _ as DbRow, a as ValidatorMode, b as FieldMeta, c as AggregateResult, d as BucketUnit, f as CalendarBucketLabel, g as DbPatch, h as DataOf, j as ServerError, k as RowOf, l as AtscriptClientShape, m as ClientResponse, n as ClientValidator, p as ClientOptions, r as ClientValidatorOptions, s as AggregateQuery, t as ClientValidationError, u as BucketExpr, v as DecoratedControls, w as NavOf, x as FilterExpr, y as DecoratedQuery, z as TypedWithRelation } from "./validator-CPmmXTAt.mjs";
2
2
  import { TAtscriptAnnotatedType, TSerializedAnnotatedType } from "@atscript/typescript/utils";
3
- import { AggregateExpr, AggregateQuery as AggregateQuery$1, AggregateResult as AggregateResult$1, BucketExpr as BucketExpr$1, Uniquery as Uniquery$1, UniqueryControls as UniqueryControls$1, ValidGroupBy as ValidGroupBy$1, bucketStartInstant, nextBucketLabel } from "@uniqu/core";
4
- import { TCrudOp, TCrudPermissions, TDbActionInfo, TDbActionIntent, TDbActionLevel, TDbActionProcessor, TDbAvailableActions, TDbAvailableActions as TDbAvailableActions$1, TDbDeleteResult as TDbDeleteResult$1, TDbInsertManyResult as TDbInsertManyResult$1, TDbInsertResult as TDbInsertResult$1, TDbUpdateResult as TDbUpdateResult$1 } from "@atscript/db";
3
+ import { AggregateExpr, AggregateOfExpr, AggregateQuery as AggregateQuery$1, AggregateResult as AggregateResult$1, BucketExpr as BucketExpr$1, BucketSeriesOptions, NextBucketOptions, SelectArithExpr, Uniquery as Uniquery$1, ValidGroupBy as ValidGroupBy$1, bucketSeries, bucketStartInstant, nextBucketLabel } from "@uniqu/core";
4
+ import { TCrudOp, TCrudPermissions, TDbActionInfo, TDbActionInfo as TDbActionInfo$1, TDbActionIntent, TDbActionLevel, TDbActionProcessor, TDbActionTargetSummary, TDbActionTargetSummary as TDbActionTargetSummary$1, TDbAvailableActions, TDbAvailableActions as TDbAvailableActions$1, TDbDeleteResult as TDbDeleteResult$1, TDbInsertIgnoreResult as TDbInsertIgnoreResult$1, TDbInsertManyIgnoreResult as TDbInsertManyIgnoreResult$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>;
8
8
  type Nav<T> = NavOf<T>;
9
9
  type Id<T> = IdOf<T>;
10
10
  type Response<T, Q> = ClientResponse<T, Q>;
11
+ /** A read row: the response over the own fields plus the declared decorations `D`, each optional. */
12
+ type DecoratedRow<T, Q, D> = Response<T, Q> & Partial<D>;
11
13
  /**
12
14
  * HTTP client for moost-db REST endpoints.
13
15
  *
@@ -28,8 +30,19 @@ type Response<T, Q> = ClientResponse<T, Q>;
28
30
  * const all = await users.query()
29
31
  * const page = await users.pages({ filter: { active: true } }, 1, 20)
30
32
  * ```
33
+ *
34
+ * `D` (since 0.1.148) types the display-only fields the controller declares
35
+ * with `@DbDecorations`: `$select` of the read methods accepts their keys and
36
+ * rows carry them as optional properties. Filter and sort stay over `T`'s own
37
+ * fields — a decoration is never filterable or sortable.
38
+ *
39
+ * ```typescript
40
+ * const tickets = new Client<typeof Ticket, TicketDecorations>('/api/tickets')
41
+ * const rows = await tickets.query({ controls: { $select: ['title', 'ownerName'] } })
42
+ * rows[0].ownerName // string | undefined
43
+ * ```
31
44
  */
32
- declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
45
+ declare class Client<T extends AtscriptClientShape = AtscriptClientShape, D extends object = Record<never, never>> {
33
46
  private readonly _path;
34
47
  private readonly _baseUrl;
35
48
  private readonly _fetch;
@@ -47,7 +60,7 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
47
60
  * The response type narrows by the literal `$with` array in `query` —
48
61
  * relations not listed in `$with` are stripped from the row type.
49
62
  */
50
- query<Q extends Uniquery$1<Own<T>, Nav<T>> = Uniquery$1<Own<T>, Nav<T>>>(query?: Q): Promise<Response<T, Q>[]>;
63
+ query<Q extends DecoratedQuery<T, D> = DecoratedQuery<T, D>>(query?: Q): Promise<DecoratedRow<T, Q, D>[]>;
51
64
  /**
52
65
  * `GET /query` with `$count: true` — returns record count.
53
66
  */
@@ -59,17 +72,24 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
59
72
  *
60
73
  * `$select` may carry calendar buckets (`{ $bucket, $field, $tz?, $weekStart?, $as? }`);
61
74
  * a `$groupBy` entry must be a dimension or a bucket alias (`ValidGroupBy`), and a
62
- * bucket's value is typed as its `YYYY-MM-DD` label (`| null` for an optional source).
75
+ * bucket's value is typed as its `YYYY-MM-DD` (hour: `YYYY-MM-DDTHH:00`) label
76
+ * (`| null` for an optional source).
63
77
  * Gap-fill between labels with `nextBucketLabel` (re-exported here).
78
+ *
79
+ * Arithmetic (since 0.1.148): `{ $fn: 'sum', $expr, $as }` aggregates a per-row
80
+ * expression over numeric fields; `{ $expr, $as }` computes over aliases of other
81
+ * numeric entries or numeric `$groupBy` fields (`number | null`). `first` / `last`
82
+ * read a representative row ordered by `$rowOrder`. The URL forms are `expr(a/b):x`,
83
+ * `sum(a*b):x`, `first(f):x` and `$rowOrder=f,-id`.
64
84
  */
65
- aggregate<const Q extends AggregateQuery$1<Own<T>>>(query: Q & ValidGroupBy$1<Own<T>, Q>): Promise<Q["controls"]["$select"] extends readonly (string | AggregateExpr | BucketExpr$1)[] ? AggregateResult$1<Own<T>, Q["controls"]["$select"]>[] : Record<string, unknown>[]>;
85
+ aggregate<const Q extends AggregateQuery$1<Own<T>>>(query: Q & ValidGroupBy$1<Own<T>, Q>): Promise<Q["controls"]["$select"] extends readonly (string | AggregateExpr | BucketExpr$1 | AggregateOfExpr | SelectArithExpr)[] ? AggregateResult$1<Own<T>, Q["controls"]["$select"]>[] : Record<string, unknown>[]>;
66
86
  /**
67
87
  * `GET /pages` — paginated query with typed filter and relations.
68
88
  *
69
89
  * Response rows narrow by the literal `$with` array — same algebra as
70
90
  * {@link query}.
71
91
  */
72
- 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>>>;
92
+ pages<Q extends DecoratedQuery<T, D> = DecoratedQuery<T, D>>(query?: Q, page?: number, size?: number): Promise<PageResult<DecoratedRow<T, Q, D>>>;
73
93
  /**
74
94
  * `GET /geo` — distance-ranked geospatial search (tables with `@db.index.geo`).
75
95
  *
@@ -79,13 +99,13 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
79
99
  * in `query.controls`; filter / `$select` / `$with` / `$skip` / `$limit`
80
100
  * compose as usual.
81
101
  */
82
- geoSearch<Q extends Uniquery$1<Own<T>, Nav<T>> = Uniquery$1<Own<T>, Nav<T>>>(point: [number, number], query?: Q): Promise<Array<Response<T, Q> & {
102
+ geoSearch<Q extends DecoratedQuery<T, D> = DecoratedQuery<T, D>>(point: [number, number], query?: Q): Promise<Array<DecoratedRow<T, Q, D> & {
83
103
  $distance: number;
84
104
  }>>;
85
105
  /**
86
106
  * `GET /geo` with `$page` / `$size` — paginated distance-ranked search.
87
107
  */
88
- geoPages<Q extends Uniquery$1<Own<T>, Nav<T>> = Uniquery$1<Own<T>, Nav<T>>>(point: [number, number], query?: Q, page?: number, size?: number): Promise<PageResult<Response<T, Q> & {
108
+ geoPages<Q extends DecoratedQuery<T, D> = DecoratedQuery<T, D>>(point: [number, number], query?: Q, page?: number, size?: number): Promise<PageResult<DecoratedRow<T, Q, D> & {
89
109
  $distance: number;
90
110
  }>>;
91
111
  /**
@@ -95,10 +115,10 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
95
115
  * `query.controls` — same algebra as {@link query}.
96
116
  */
97
117
  one<Q extends {
98
- controls?: UniqueryControls$1<Own<T>, Nav<T>>;
118
+ controls?: DecoratedControls<T, D>;
99
119
  } = {
100
- controls?: UniqueryControls$1<Own<T>, Nav<T>>;
101
- }>(id: Id<T>, query?: Q): Promise<Response<T, Q> | null>;
120
+ controls?: DecoratedControls<T, D>;
121
+ }>(id: Id<T>, query?: Q): Promise<DecoratedRow<T, Q, D> | null>;
102
122
  /**
103
123
  * `POST /` — insert one record. The version column (if any) is optional —
104
124
  * the server initialises it; `$cas` is rejected (no meaning on insert).
@@ -108,6 +128,20 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
108
128
  * `POST /` — insert many records.
109
129
  */
110
130
  insert(data: PatchOf<T>[]): Promise<TDbInsertManyResult$1>;
131
+ /**
132
+ * `POST /?$onConflict=ignore` — insert one record, skipping it when it
133
+ * collides on the primary key or a unique index (since 0.1.148).
134
+ */
135
+ insert(data: PatchOf<T>, opts: {
136
+ onConflict: "ignore";
137
+ }): Promise<TDbInsertIgnoreResult$1>;
138
+ /**
139
+ * `POST /?$onConflict=ignore` — insert many records, skipping the ones that
140
+ * collide; the result reports inserted and skipped input indices (since 0.1.148).
141
+ */
142
+ insert(data: PatchOf<T>[], opts: {
143
+ onConflict: "ignore";
144
+ }): Promise<TDbInsertManyIgnoreResult$1>;
111
145
  /**
112
146
  * `PATCH /` — partial update one or many records by primary key.
113
147
  *
@@ -166,11 +200,48 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
166
200
  * `{ ids?, input? }` — `ids` carries `id` (object or array per level),
167
201
  * `input` carries the form payload.
168
202
  *
203
+ * **Delegated actions** (since 0.1.147). An action another controller owns
204
+ * (`owner`, e.g. a view listing its source table's actions) may carry
205
+ * `idMap`: each `id` is then a row (or identifier) of THIS controller and
206
+ * is mapped to the owner's identification — `{ [ownerField]:
207
+ * row[path] }` — before it is sent to `value` (the owner's route). A
208
+ * missing path throws `TypeError`. See {@link actionIdentifier}.
209
+ *
169
210
  * @typeParam R Caller-asserted return shape from the action handler. The
170
211
  * server returns whatever the handler emits (commonly
171
212
  * `{ message?: string, ... }`); the client cannot validate.
172
213
  */
173
214
  action<R = unknown>(name: string, id?: Partial<Own<T>> | Partial<Own<T>>[], input?: unknown): Promise<R>;
215
+ /**
216
+ * Runs a `'rows'` action on every row matching a query (since 0.1.147) —
217
+ * the action's `/meta` entry must carry `queryTarget`. POSTs
218
+ * `{ query: { q, exclude?, expectCount?, maxRows? }, input? }` to
219
+ * `queryTarget.url` (a delegated action: the view resolves the rows and
220
+ * runs the owner's action in batches) or `value`. `q` is the `/query`
221
+ * string of `target.filter` / `search` / `index`.
222
+ *
223
+ * The server answers what the handler returns — for a delegated action
224
+ * (and a handler returning `target.summary()`) a
225
+ * {@link TDbActionTargetSummary}. Refusals arrive as
226
+ * {@link ActionTargetError}: `TARGET_TOO_LARGE` (`cap`), `TARGET_CHANGED`
227
+ * (`matched` differs from `expectCount`), `TARGET_INVALID`. Throws
228
+ * {@link ActionUnsupportedError} when the action takes no query target.
229
+ *
230
+ * @since 0.1.147
231
+ */
232
+ actionOnQuery<R = TDbActionTargetSummary$1>(name: string, target: TDbQueryTarget<T>, input?: unknown): Promise<R>;
233
+ /**
234
+ * How many rows {@link actionOnQuery} would target right now (a dry run:
235
+ * the handler does not run) — pass the answer as `expectCount` to make the
236
+ * real run fail with `TARGET_CHANGED` if the set changed in between.
237
+ *
238
+ * @since 0.1.147
239
+ */
240
+ countActionTarget(name: string, target: TDbQueryTarget<T>): Promise<{
241
+ matched: number;
242
+ }>;
243
+ private _queryTargetAction;
244
+ private _postQueryTarget;
174
245
  /**
175
246
  * `GET /meta/actions/:id` or `GET /meta/actions?k1=v1&k2=v2` — the
176
247
  * row-level actions the caller may run on one row right now, and the
@@ -224,6 +295,22 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape> {
224
295
  private _requestUrl;
225
296
  private _send;
226
297
  }
298
+ /**
299
+ * The identifier `action` takes for one row (or identifier) of the
300
+ * controller whose `/meta` listed it (since 0.1.147):
301
+ *
302
+ * - a delegated action with `idMap` → the owner's identification,
303
+ * `{ [ownerField]: rowOrId[path] }` (dot paths allowed); a missing value
304
+ * throws `TypeError` naming the action and the path;
305
+ * - any other action → `rowOrId`'s `preferredId` fields when it carries them
306
+ * all, else `rowOrId` itself (already an identifier).
307
+ *
308
+ * UIs build `ids` from loaded rows with it; `Client.action()` applies the
309
+ * `idMap` mapping itself.
310
+ *
311
+ * @since 0.1.147
312
+ */
313
+ declare function actionIdentifier(action: Pick<TDbActionInfo$1, "name" | "idMap">, rowOrId: Record<string, unknown>, preferredId: readonly string[]): Record<string, unknown>;
227
314
  /**
228
315
  * Render a single identifier field for substitution into a navigate-URL
229
316
  * template or human-readable string. `null` / `undefined` collapse to `""`
@@ -318,6 +405,34 @@ declare class ActionDisabledError extends ClientError {
318
405
  */
319
406
  get reasons(): (string | null)[] | undefined;
320
407
  }
408
+ /**
409
+ * Wire-body shape for `ActionTargetError` responses (since 0.1.147): a
410
+ * query target the server refused — `code` says why.
411
+ */
412
+ interface ActionTargetErrorBody extends ServerError {
413
+ name: "ActionTargetError";
414
+ code: "TARGET_INVALID" | "TARGET_TOO_LARGE" | "TARGET_CHANGED";
415
+ action: string;
416
+ /** `TARGET_CHANGED`: the current match count. */
417
+ matched?: number;
418
+ /** `TARGET_TOO_LARGE`: the most rows one request may target. */
419
+ cap?: number;
420
+ }
421
+ /**
422
+ * Typed marker thrown by `Client` when a query-targeted action request is
423
+ * refused (body `name === 'ActionTargetError'`): `TARGET_INVALID` (400),
424
+ * `TARGET_TOO_LARGE` (400, `cap`) or `TARGET_CHANGED` (409, `matched` — the
425
+ * rows the query matches now; re-confirm and retry with that count).
426
+ *
427
+ * @since 0.1.147
428
+ */
429
+ declare class ActionTargetError extends ClientError {
430
+ name: string;
431
+ get code(): ActionTargetErrorBody["code"];
432
+ get action(): string;
433
+ get matched(): number | undefined;
434
+ get cap(): number | undefined;
435
+ }
321
436
  /**
322
437
  * Wire-body shape for 409 OCC `version_mismatch` responses. Extends the base
323
438
  * `ServerError` envelope with a `kind` discriminator and the row's current
@@ -382,4 +497,4 @@ declare class ActionUnsupportedError extends Error {
382
497
  constructor(action: string, processor: string, message: string);
383
498
  }
384
499
  //#endregion
385
- export { ActionDisabledError, type ActionDisabledErrorBody, ActionNotFoundError, ActionUnsupportedError, type AggregateQuery, type AggregateResult, type AtscriptClientShape, type BucketExpr, type BucketUnit, type CalendarBucketLabel, Client, ClientError, type ClientOptions, type ClientResponse, type ClientValidationError, type ClientValidator, type ClientValidatorOptions, type DataOf, type DbPatch, type DbRow, type FieldMeta, type FilterExpr, type IdOf, type MetaResponse, type NavOf, type OwnOf, type PageResult, type PatchOf, type RelationInfo, type RowOf, type SearchIndexInfo, type ServerError, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbAvailableActions, type TDbDeleteResult, type TDbInsertManyResult, type TDbInsertResult, type TDbUpdateResult, type TSerializedAnnotatedType, TransportError, type TypedWithRelation, type Uniquery, type UniqueryControls, type ValidGroupBy, type ValidatorMode, VersionMismatchError, type VersionMismatchErrorBody, type WeekStart, bucketStartInstant, encodeNavigateId, formatIdentifier, formatIdentifierField, nextBucketLabel };
500
+ export { ActionDisabledError, type ActionDisabledErrorBody, ActionNotFoundError, ActionTargetError, type ActionTargetErrorBody, ActionUnsupportedError, type AggregateQuery, type AggregateResult, type AtscriptClientShape, type BucketExpr, type BucketSeriesOptions, type BucketUnit, type CalendarBucketLabel, Client, ClientError, type ClientOptions, type ClientResponse, type ClientValidationError, type ClientValidator, type ClientValidatorOptions, type DataOf, type DbPatch, type DbRow, type FieldMeta, type FilterExpr, type IdOf, type MetaResponse, type NavOf, type NextBucketOptions, type OwnOf, type PageResult, type PatchOf, type RelationInfo, type RowOf, type SearchIndexInfo, type ServerError, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbActionTargetSummary, type TDbAvailableActions, type TDbDeleteResult, type TDbInsertIgnoreResult, type TDbInsertManyIgnoreResult, type TDbInsertManyResult, type TDbInsertResult, type TDbQueryTarget, type TDbUpdateResult, type TSerializedAnnotatedType, TransportError, type TypedWithRelation, type Uniquery, type UniqueryControls, type ValidGroupBy, type ValidatorMode, VersionMismatchError, type VersionMismatchErrorBody, type WeekStart, actionIdentifier, bucketSeries, bucketStartInstant, encodeNavigateId, formatIdentifier, formatIdentifierField, nextBucketLabel };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { buildUrl } from "@uniqu/url/builder";
2
2
  import { deserializeAnnotatedType } from "@atscript/typescript/utils";
3
- import { bucketStartInstant, nextBucketLabel } from "@uniqu/core";
3
+ import { bucketSeries, bucketStartInstant, nextBucketLabel } from "@uniqu/core";
4
4
  //#region src/client-error.ts
5
5
  /**
6
6
  * Error thrown by `Client` when the server responds with a non-2xx status code.
@@ -64,6 +64,29 @@ var ActionDisabledError = class extends ClientError {
64
64
  }
65
65
  };
66
66
  /**
67
+ * Typed marker thrown by `Client` when a query-targeted action request is
68
+ * refused (body `name === 'ActionTargetError'`): `TARGET_INVALID` (400),
69
+ * `TARGET_TOO_LARGE` (400, `cap`) or `TARGET_CHANGED` (409, `matched` — the
70
+ * rows the query matches now; re-confirm and retry with that count).
71
+ *
72
+ * @since 0.1.147
73
+ */
74
+ var ActionTargetError = class extends ClientError {
75
+ name = "ActionTargetError";
76
+ get code() {
77
+ return this.body.code;
78
+ }
79
+ get action() {
80
+ return this.body.action;
81
+ }
82
+ get matched() {
83
+ return this.body.matched;
84
+ }
85
+ get cap() {
86
+ return this.body.cap;
87
+ }
88
+ };
89
+ /**
67
90
  * Typed marker thrown by `Client._send` when the server response body's
68
91
  * `kind === 'version_mismatch'`. The transport / status / base body are
69
92
  * identical to a generic `ClientError`; this subclass adds a typed
@@ -142,6 +165,17 @@ var ActionUnsupportedError = class extends Error {
142
165
  * const all = await users.query()
143
166
  * const page = await users.pages({ filter: { active: true } }, 1, 20)
144
167
  * ```
168
+ *
169
+ * `D` (since 0.1.148) types the display-only fields the controller declares
170
+ * with `@DbDecorations`: `$select` of the read methods accepts their keys and
171
+ * rows carry them as optional properties. Filter and sort stay over `T`'s own
172
+ * fields — a decoration is never filterable or sortable.
173
+ *
174
+ * ```typescript
175
+ * const tickets = new Client<typeof Ticket, TicketDecorations>('/api/tickets')
176
+ * const rows = await tickets.query({ controls: { $select: ['title', 'ownerName'] } })
177
+ * rows[0].ownerName // string | undefined
178
+ * ```
145
179
  */
146
180
  var Client = class {
147
181
  _path;
@@ -185,8 +219,15 @@ var Client = class {
185
219
  *
186
220
  * `$select` may carry calendar buckets (`{ $bucket, $field, $tz?, $weekStart?, $as? }`);
187
221
  * a `$groupBy` entry must be a dimension or a bucket alias (`ValidGroupBy`), and a
188
- * bucket's value is typed as its `YYYY-MM-DD` label (`| null` for an optional source).
222
+ * bucket's value is typed as its `YYYY-MM-DD` (hour: `YYYY-MM-DDTHH:00`) label
223
+ * (`| null` for an optional source).
189
224
  * Gap-fill between labels with `nextBucketLabel` (re-exported here).
225
+ *
226
+ * Arithmetic (since 0.1.148): `{ $fn: 'sum', $expr, $as }` aggregates a per-row
227
+ * expression over numeric fields; `{ $expr, $as }` computes over aliases of other
228
+ * numeric entries or numeric `$groupBy` fields (`number | null`). `first` / `last`
229
+ * read a representative row ordered by `$rowOrder`. The URL forms are `expr(a/b):x`,
230
+ * `sum(a*b):x`, `first(f):x` and `$rowOrder=f,-id`.
190
231
  */
191
232
  async aggregate(query) {
192
233
  return this._get("query", query);
@@ -249,8 +290,9 @@ var Client = class {
249
290
  const controlStr = query?.controls ? buildUrl({ controls: query.controls }) : "";
250
291
  return this._getOrNull(this._idUrl("one", id, controlStr));
251
292
  }
252
- async insert(data) {
253
- return this._request("POST", "", await this._prepareWrite(data, "insert"));
293
+ async insert(data, opts) {
294
+ const query = opts?.onConflict === "ignore" ? "?$onConflict=ignore" : "";
295
+ return this._request("POST", query, await this._prepareWrite(data, "insert"));
254
296
  }
255
297
  /**
256
298
  * `PATCH /` — partial update one or many records by primary key.
@@ -322,6 +364,13 @@ var Client = class {
322
364
  * `{ ids?, input? }` — `ids` carries `id` (object or array per level),
323
365
  * `input` carries the form payload.
324
366
  *
367
+ * **Delegated actions** (since 0.1.147). An action another controller owns
368
+ * (`owner`, e.g. a view listing its source table's actions) may carry
369
+ * `idMap`: each `id` is then a row (or identifier) of THIS controller and
370
+ * is mapped to the owner's identification — `{ [ownerField]:
371
+ * row[path] }` — before it is sent to `value` (the owner's route). A
372
+ * missing path throws `TypeError`. See {@link actionIdentifier}.
373
+ *
325
374
  * @typeParam R Caller-asserted return shape from the action handler. The
326
375
  * server returns whatever the handler emits (commonly
327
376
  * `{ message?: string, ... }`); the client cannot validate.
@@ -331,15 +380,65 @@ var Client = class {
331
380
  const action = meta.actions.find((a) => a.name === name);
332
381
  if (!action) throw new ActionNotFoundError(name);
333
382
  if (action.processor === "custom") throw new ActionUnsupportedError(name, "custom", `Action "${name}" has processor "custom" — applications must dispatch custom actions themselves; the client cannot.`);
383
+ const mapped = action.idMap ? mapDelegatedIds(action, id) : id;
334
384
  if (action.processor === "navigate") {
335
- const url = this._interpolateNavigateUrl(action, id, meta.preferredId);
385
+ const order = action.idMap ? Object.keys(action.idMap) : meta.preferredId;
386
+ const url = this._interpolateNavigateUrl(action, mapped, order);
336
387
  await this._dispatchNavigate(action, url);
337
388
  return;
338
389
  }
339
- const body = this._buildActionBody(action, id, input);
390
+ const body = this._buildActionBody(action, mapped, input);
340
391
  return this._postAction(action, body);
341
392
  }
342
393
  /**
394
+ * Runs a `'rows'` action on every row matching a query (since 0.1.147) —
395
+ * the action's `/meta` entry must carry `queryTarget`. POSTs
396
+ * `{ query: { q, exclude?, expectCount?, maxRows? }, input? }` to
397
+ * `queryTarget.url` (a delegated action: the view resolves the rows and
398
+ * runs the owner's action in batches) or `value`. `q` is the `/query`
399
+ * string of `target.filter` / `search` / `index`.
400
+ *
401
+ * The server answers what the handler returns — for a delegated action
402
+ * (and a handler returning `target.summary()`) a
403
+ * {@link TDbActionTargetSummary}. Refusals arrive as
404
+ * {@link ActionTargetError}: `TARGET_TOO_LARGE` (`cap`), `TARGET_CHANGED`
405
+ * (`matched` differs from `expectCount`), `TARGET_INVALID`. Throws
406
+ * {@link ActionUnsupportedError} when the action takes no query target.
407
+ *
408
+ * @since 0.1.147
409
+ */
410
+ async actionOnQuery(name, target, input) {
411
+ const action = await this._queryTargetAction(name);
412
+ const body = { query: queryTargetBody(target) };
413
+ if (input !== void 0) body.input = input;
414
+ return this._postQueryTarget(action, body);
415
+ }
416
+ /**
417
+ * How many rows {@link actionOnQuery} would target right now (a dry run:
418
+ * the handler does not run) — pass the answer as `expectCount` to make the
419
+ * real run fail with `TARGET_CHANGED` if the set changed in between.
420
+ *
421
+ * @since 0.1.147
422
+ */
423
+ async countActionTarget(name, target) {
424
+ const action = await this._queryTargetAction(name);
425
+ const query = {
426
+ ...queryTargetBody(target),
427
+ dryRun: true
428
+ };
429
+ return this._postQueryTarget(action, { query });
430
+ }
431
+ async _queryTargetAction(name) {
432
+ const action = (await this.meta()).actions.find((a) => a.name === name);
433
+ if (!action) throw new ActionNotFoundError(name);
434
+ if (!action.queryTarget || action.processor !== "backend") throw new ActionUnsupportedError(name, action.processor, `Action "${name}" does not accept a query target (no \`queryTarget\` in /meta).`);
435
+ return action;
436
+ }
437
+ _postQueryTarget(action, body) {
438
+ const path = action.queryTarget?.url ?? action.value;
439
+ return this._requestUrl("POST", `${this._baseUrl}${path}`, body, true);
440
+ }
441
+ /**
343
442
  * `GET /meta/actions/:id` or `GET /meta/actions?k1=v1&k2=v2` — the
344
443
  * row-level actions the caller may run on one row right now, and the
345
444
  * reasons of those disabled with one: `{ actions, disabledReasons? }`.
@@ -518,6 +617,7 @@ var Client = class {
518
617
  };
519
618
  }
520
619
  if (errorBody.name === "ActionDisabledError") throw new ActionDisabledError(res.status, errorBody);
620
+ if (errorBody.name === "ActionTargetError") throw new ActionTargetError(res.status, errorBody);
521
621
  if (errorBody.kind === "version_mismatch") throw new VersionMismatchError(res.status, errorBody);
522
622
  throw new ClientError(res.status, errorBody);
523
623
  }
@@ -539,6 +639,60 @@ function describeCause(cause) {
539
639
  if (cause instanceof Error) return cause.message || cause.name;
540
640
  return String(cause);
541
641
  }
642
+ /** The value at a dot `path` of `row`. */
643
+ function valueAt(row, path) {
644
+ if (!path.includes(".") || Object.hasOwn(row, path)) return row[path];
645
+ let v = row;
646
+ for (const part of path.split(".")) v = v?.[part];
647
+ return v;
648
+ }
649
+ /**
650
+ * The identifier `action` takes for one row (or identifier) of the
651
+ * controller whose `/meta` listed it (since 0.1.147):
652
+ *
653
+ * - a delegated action with `idMap` → the owner's identification,
654
+ * `{ [ownerField]: rowOrId[path] }` (dot paths allowed); a missing value
655
+ * throws `TypeError` naming the action and the path;
656
+ * - any other action → `rowOrId`'s `preferredId` fields when it carries them
657
+ * all, else `rowOrId` itself (already an identifier).
658
+ *
659
+ * UIs build `ids` from loaded rows with it; `Client.action()` applies the
660
+ * `idMap` mapping itself.
661
+ *
662
+ * @since 0.1.147
663
+ */
664
+ function actionIdentifier(action, rowOrId, preferredId) {
665
+ if (action.idMap) {
666
+ const out = {};
667
+ for (const [field, path] of Object.entries(action.idMap)) {
668
+ const value = valueAt(rowOrId, path);
669
+ if (value === void 0 || value === null) throw new TypeError(`client.action("${action.name}"): the identifier has no "${path}" — needed for the owner's "${field}".`);
670
+ out[field] = value;
671
+ }
672
+ return out;
673
+ }
674
+ if (preferredId.length > 0 && preferredId.every((f) => rowOrId[f] !== void 0)) return Object.fromEntries(preferredId.map((f) => [f, rowOrId[f]]));
675
+ return rowOrId;
676
+ }
677
+ /** `action()`'s `id` argument through a delegated action's `idMap` (shape errors are left to the body builder). */
678
+ function mapDelegatedIds(action, id) {
679
+ const map = (one) => one !== null && typeof one === "object" && !Array.isArray(one) ? actionIdentifier(action, one, []) : one;
680
+ return Array.isArray(id) ? id.map(map) : map(id);
681
+ }
682
+ /** The wire `query` of a {@link TDbQueryTarget}. */
683
+ function queryTargetBody(target) {
684
+ const controls = {};
685
+ if (target.search !== void 0) controls.$search = target.search;
686
+ if (target.index !== void 0) controls.$index = target.index;
687
+ const out = { q: buildUrl({
688
+ filter: target.filter ?? {},
689
+ controls
690
+ }) };
691
+ if (target.exclude?.length) out.exclude = target.exclude;
692
+ if (target.expectCount !== void 0) out.expectCount = target.expectCount;
693
+ if (target.maxRows !== void 0) out.maxRows = target.maxRows;
694
+ return out;
695
+ }
542
696
  /**
543
697
  * Render a single identifier field for substitution into a navigate-URL
544
698
  * template or human-readable string. `null` / `undefined` collapse to `""`
@@ -575,4 +729,4 @@ function describeShape(value) {
575
729
  return typeof value;
576
730
  }
577
731
  //#endregion
578
- export { ActionDisabledError, ActionNotFoundError, ActionUnsupportedError, Client, ClientError, TransportError, VersionMismatchError, bucketStartInstant, encodeNavigateId, formatIdentifier, formatIdentifierField, nextBucketLabel };
732
+ export { ActionDisabledError, ActionNotFoundError, ActionTargetError, ActionUnsupportedError, Client, ClientError, TransportError, VersionMismatchError, actionIdentifier, bucketSeries, bucketStartInstant, encodeNavigateId, formatIdentifier, formatIdentifierField, nextBucketLabel };
@@ -1,7 +1,7 @@
1
- import { AggregateQuery as AggregateQuery$1, AggregateResult as AggregateResult$1, BucketExpr as BucketExpr$1, BucketUnit, CalendarBucketLabel, FilterExpr, TypedWithRelation, Uniquery as Uniquery$1, UniqueryControls as UniqueryControls$1, ValidGroupBy as ValidGroupBy$1, WeekStart } from "@uniqu/core";
2
1
  import { TAtscriptAnnotatedType, TAtscriptTypeObject } from "@atscript/typescript/utils";
3
- import { DbPatch, DbResponse, DbRow, 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 { AggregateQuery as AggregateQuery$1, AggregateResult as AggregateResult$1, BucketExpr as BucketExpr$1, BucketUnit, CalendarBucketLabel, FilterExpr, SelectExpr, TypedWithRelation, Uniquery as Uniquery$1, UniqueryControls, ValidGroupBy as ValidGroupBy$1, WeekStart } from "@uniqu/core";
4
3
  import { DbValidationContext, ValidatorMode, ValidatorMode as ValidatorMode$1 } from "@atscript/db/validator";
4
+ import { DbPatch, DbResponse, DbRow, TDbDeleteResult as TDbDeleteResult$1, TDbInsertIgnoreResult as TDbInsertIgnoreResult$1, TDbInsertManyIgnoreResult as TDbInsertManyIgnoreResult$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
  /**
@@ -96,6 +96,18 @@ type DataOf<T> = T extends {
96
96
  __dataType?: infer D;
97
97
  };
98
98
  } ? unknown extends D ? T extends (new (...a: any[]) => infer I) ? I : Record<string, unknown> : D & Record<string, unknown> : Record<string, unknown>;
99
+ /**
100
+ * `$select` controls of a read over `T` whose controller declares the display-only
101
+ * fields `D` (`@DbDecorations`): `$select` also accepts `keyof D`; filter, sort
102
+ * and every other control stay over the own fields. Since 0.1.148.
103
+ */
104
+ type DecoratedControls<T, D> = Omit<UniqueryControls<OwnOf<T>, NavOf<T>>, "$select"> & {
105
+ $select?: SelectExpr<OwnOf<T> & D>;
106
+ };
107
+ /** A read query over `T` — {@link DecoratedControls} for the controls. Since 0.1.148. */
108
+ type DecoratedQuery<T, D> = Omit<Uniquery$1<OwnOf<T>, NavOf<T>>, "controls"> & {
109
+ controls?: DecoratedControls<T, D>;
110
+ };
99
111
  /** Extract own (non-nav) properties from an Atscript annotated type. */
100
112
  type OwnOf<T> = T extends {
101
113
  __ownProps: infer O;
@@ -132,6 +144,28 @@ type ClientResponse<T, Q> = DbResponse<DataOf<T>, NavOf<T>, Q> & {
132
144
  */
133
145
  $disabledReasons?: Record<string, string>;
134
146
  };
147
+ /**
148
+ * "Every row matching this query" — the target of
149
+ * `Client.actionOnQuery()` / `countActionTarget()`, for a `'rows'` action
150
+ * whose `/meta` entry carries `queryTarget`. The same filter / search /
151
+ * index the user's `/query` used; the server resolves it under the caller's
152
+ * read scope and the action's own gate.
153
+ *
154
+ * @since 0.1.147
155
+ */
156
+ interface TDbQueryTarget<T = AtscriptClientShape> {
157
+ filter?: Uniquery$1<OwnOf<T>, NavOf<T>>["filter"];
158
+ /** `$search` term. */
159
+ search?: string;
160
+ /** `$index` — the search index `search` uses. */
161
+ index?: string;
162
+ /** Identifiers to leave out (any identification of the controller the request goes to). */
163
+ exclude?: Record<string, unknown>[];
164
+ /** Fail with 409 `TARGET_CHANGED` when the target no longer matches exactly this many rows. */
165
+ expectCount?: number;
166
+ /** Client-side cap (never above the action's `queryTarget.maxRows`). */
167
+ maxRows?: number;
168
+ }
135
169
  //#endregion
136
170
  //#region src/validator.d.ts
137
171
  /** Options for {@link ClientValidator} / {@link createClientValidator}. */
@@ -214,4 +248,4 @@ declare class ClientValidationError extends Error {
214
248
  */
215
249
  declare function createClientValidator(meta: MetaResponse, opts?: ClientValidatorOptions): ClientValidator;
216
250
  //#endregion
217
- export { TDbDeleteResult$1 as A, OwnOf as C, RowOf as D, RelationInfo as E, Uniquery$1 as F, UniqueryControls$1 as I, ValidGroupBy$1 as L, TDbInsertResult$1 as M, TDbUpdateResult$1 as N, SearchIndexInfo as O, TypedWithRelation as P, WeekStart as R, NavOf as S, PatchOf as T, DbRow as _, ValidatorMode$1 as a, IdOf as b, AggregateResult$1 as c, BucketUnit as d, CalendarBucketLabel as f, DbPatch as g, DataOf as h, DbValidationContext as i, TDbInsertManyResult$1 as j, ServerError as k, AtscriptClientShape as l, ClientResponse as m, ClientValidator as n, createClientValidator as o, ClientOptions as p, ClientValidatorOptions as r, AggregateQuery$1 as s, ClientValidationError as t, BucketExpr$1 as u, FieldMeta as v, PageResult as w, MetaResponse as x, FilterExpr as y };
251
+ export { SearchIndexInfo as A, Uniquery$1 as B, MetaResponse as C, PatchOf as D, PageResult as E, TDbInsertManyResult$1 as F, ValidGroupBy$1 as H, TDbInsertResult$1 as I, TDbQueryTarget as L, TDbDeleteResult$1 as M, TDbInsertIgnoreResult$1 as N, RelationInfo as O, TDbInsertManyIgnoreResult$1 as P, TDbUpdateResult$1 as R, IdOf as S, OwnOf as T, WeekStart as U, UniqueryControls as V, DbRow as _, ValidatorMode$1 as a, FieldMeta as b, AggregateResult$1 as c, BucketUnit as d, CalendarBucketLabel as f, DbPatch as g, DataOf as h, DbValidationContext as i, ServerError as j, RowOf as k, AtscriptClientShape as l, ClientResponse as m, ClientValidator as n, createClientValidator as o, ClientOptions as p, ClientValidatorOptions as r, AggregateQuery$1 as s, ClientValidationError as t, BucketExpr$1 as u, DecoratedControls as v, NavOf as w, FilterExpr as x, DecoratedQuery as y, TypedWithRelation as z };
@@ -1,7 +1,7 @@
1
+ import { AggregateQuery as AggregateQuery$1, AggregateResult as AggregateResult$1, BucketExpr as BucketExpr$1, BucketUnit, CalendarBucketLabel, FilterExpr, SelectExpr, TypedWithRelation, Uniquery as Uniquery$1, UniqueryControls, ValidGroupBy as ValidGroupBy$1, WeekStart } from "@uniqu/core";
1
2
  import { TAtscriptAnnotatedType, TAtscriptTypeObject } from "@atscript/typescript/utils";
2
- import { AggregateQuery as AggregateQuery$1, AggregateResult as AggregateResult$1, BucketExpr as BucketExpr$1, BucketUnit, CalendarBucketLabel, FilterExpr, TypedWithRelation, Uniquery as Uniquery$1, UniqueryControls as UniqueryControls$1, ValidGroupBy as ValidGroupBy$1, WeekStart } from "@uniqu/core";
3
+ import { DbPatch, DbResponse, DbRow, TDbDeleteResult as TDbDeleteResult$1, TDbInsertIgnoreResult as TDbInsertIgnoreResult$1, TDbInsertManyIgnoreResult as TDbInsertManyIgnoreResult$1, TDbInsertManyResult as TDbInsertManyResult$1, TDbInsertResult as TDbInsertResult$1, TDbUpdateResult as TDbUpdateResult$1, TFieldMeta, TMetaResponse, TRelationInfo, TSearchIndexInfo } from "@atscript/db";
3
4
  import { DbValidationContext, ValidatorMode, ValidatorMode as ValidatorMode$1 } from "@atscript/db/validator";
4
- import { DbPatch, DbResponse, DbRow, 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
  /**
@@ -96,6 +96,18 @@ type DataOf<T> = T extends {
96
96
  __dataType?: infer D;
97
97
  };
98
98
  } ? unknown extends D ? T extends (new (...a: any[]) => infer I) ? I : Record<string, unknown> : D & Record<string, unknown> : Record<string, unknown>;
99
+ /**
100
+ * `$select` controls of a read over `T` whose controller declares the display-only
101
+ * fields `D` (`@DbDecorations`): `$select` also accepts `keyof D`; filter, sort
102
+ * and every other control stay over the own fields. Since 0.1.148.
103
+ */
104
+ type DecoratedControls<T, D> = Omit<UniqueryControls<OwnOf<T>, NavOf<T>>, "$select"> & {
105
+ $select?: SelectExpr<OwnOf<T> & D>;
106
+ };
107
+ /** A read query over `T` — {@link DecoratedControls} for the controls. Since 0.1.148. */
108
+ type DecoratedQuery<T, D> = Omit<Uniquery$1<OwnOf<T>, NavOf<T>>, "controls"> & {
109
+ controls?: DecoratedControls<T, D>;
110
+ };
99
111
  /** Extract own (non-nav) properties from an Atscript annotated type. */
100
112
  type OwnOf<T> = T extends {
101
113
  __ownProps: infer O;
@@ -132,6 +144,28 @@ type ClientResponse<T, Q> = DbResponse<DataOf<T>, NavOf<T>, Q> & {
132
144
  */
133
145
  $disabledReasons?: Record<string, string>;
134
146
  };
147
+ /**
148
+ * "Every row matching this query" — the target of
149
+ * `Client.actionOnQuery()` / `countActionTarget()`, for a `'rows'` action
150
+ * whose `/meta` entry carries `queryTarget`. The same filter / search /
151
+ * index the user's `/query` used; the server resolves it under the caller's
152
+ * read scope and the action's own gate.
153
+ *
154
+ * @since 0.1.147
155
+ */
156
+ interface TDbQueryTarget<T = AtscriptClientShape> {
157
+ filter?: Uniquery$1<OwnOf<T>, NavOf<T>>["filter"];
158
+ /** `$search` term. */
159
+ search?: string;
160
+ /** `$index` — the search index `search` uses. */
161
+ index?: string;
162
+ /** Identifiers to leave out (any identification of the controller the request goes to). */
163
+ exclude?: Record<string, unknown>[];
164
+ /** Fail with 409 `TARGET_CHANGED` when the target no longer matches exactly this many rows. */
165
+ expectCount?: number;
166
+ /** Client-side cap (never above the action's `queryTarget.maxRows`). */
167
+ maxRows?: number;
168
+ }
135
169
  //#endregion
136
170
  //#region src/validator.d.ts
137
171
  /** Options for {@link ClientValidator} / {@link createClientValidator}. */
@@ -214,4 +248,4 @@ declare class ClientValidationError extends Error {
214
248
  */
215
249
  declare function createClientValidator(meta: MetaResponse, opts?: ClientValidatorOptions): ClientValidator;
216
250
  //#endregion
217
- export { TDbDeleteResult$1 as A, OwnOf as C, RowOf as D, RelationInfo as E, Uniquery$1 as F, UniqueryControls$1 as I, ValidGroupBy$1 as L, TDbInsertResult$1 as M, TDbUpdateResult$1 as N, SearchIndexInfo as O, TypedWithRelation as P, WeekStart as R, NavOf as S, PatchOf as T, DbRow as _, ValidatorMode$1 as a, IdOf as b, AggregateResult$1 as c, BucketUnit as d, CalendarBucketLabel as f, DbPatch as g, DataOf as h, DbValidationContext as i, TDbInsertManyResult$1 as j, ServerError as k, AtscriptClientShape as l, ClientResponse as m, ClientValidator as n, createClientValidator as o, ClientOptions as p, ClientValidatorOptions as r, AggregateQuery$1 as s, ClientValidationError as t, BucketExpr$1 as u, FieldMeta as v, PageResult as w, MetaResponse as x, FilterExpr as y };
251
+ export { SearchIndexInfo as A, Uniquery$1 as B, MetaResponse as C, PatchOf as D, PageResult as E, TDbInsertManyResult$1 as F, ValidGroupBy$1 as H, TDbInsertResult$1 as I, TDbQueryTarget as L, TDbDeleteResult$1 as M, TDbInsertIgnoreResult$1 as N, RelationInfo as O, TDbInsertManyIgnoreResult$1 as P, TDbUpdateResult$1 as R, IdOf as S, OwnOf as T, WeekStart as U, UniqueryControls as V, DbRow as _, ValidatorMode$1 as a, FieldMeta as b, AggregateResult$1 as c, BucketUnit as d, CalendarBucketLabel as f, DbPatch as g, DataOf as h, DbValidationContext as i, ServerError as j, RowOf as k, AtscriptClientShape as l, ClientResponse as m, ClientValidator as n, createClientValidator as o, ClientOptions as p, ClientValidatorOptions as r, AggregateQuery$1 as s, ClientValidationError as t, BucketExpr$1 as u, DecoratedControls as v, NavOf as w, FilterExpr as x, DecoratedQuery as y, TypedWithRelation as z };
@@ -1,2 +1,2 @@
1
- import { a as ValidatorMode, i as DbValidationContext, n as ClientValidator, o as createClientValidator, r as ClientValidatorOptions, t as ClientValidationError } from "./validator-810-rhnQ.cjs";
1
+ import { a as ValidatorMode, i as DbValidationContext, n as ClientValidator, o as createClientValidator, r as ClientValidatorOptions, t as ClientValidationError } from "./validator-CVM7eNP7.cjs";
2
2
  export { ClientValidationError, ClientValidator, ClientValidatorOptions, type DbValidationContext, type ValidatorMode, createClientValidator };
@@ -1,2 +1,2 @@
1
- import { a as ValidatorMode, i as DbValidationContext, n as ClientValidator, o as createClientValidator, r as ClientValidatorOptions, t as ClientValidationError } from "./validator-CIm1ZCS5.mjs";
1
+ import { a as ValidatorMode, i as DbValidationContext, n as ClientValidator, o as createClientValidator, r as ClientValidatorOptions, t as ClientValidationError } from "./validator-CPmmXTAt.mjs";
2
2
  export { ClientValidationError, ClientValidator, ClientValidatorOptions, type DbValidationContext, type ValidatorMode, createClientValidator };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-client",
3
- "version": "0.1.146",
3
+ "version": "0.1.148",
4
4
  "description": "Browser-compatible HTTP client for @atscript/moost-db REST endpoints.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -43,18 +43,18 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@uniqu/core": "^0.1.11",
47
- "@uniqu/url": "^0.1.11"
46
+ "@uniqu/core": "^0.1.13",
47
+ "@uniqu/url": "^0.1.13"
48
48
  },
49
49
  "devDependencies": {
50
- "@atscript/core": "^0.1.98",
51
- "@atscript/typescript": "^0.1.98",
52
- "unplugin-atscript": "^0.1.98",
53
- "@atscript/db": "0.1.146"
50
+ "@atscript/core": "^0.1.100",
51
+ "@atscript/typescript": "^0.1.100",
52
+ "unplugin-atscript": "^0.1.100",
53
+ "@atscript/db": "0.1.148"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "@atscript/db": "^0.1.44",
57
- "@atscript/typescript": "^0.1.98"
57
+ "@atscript/typescript": "^0.1.100"
58
58
  },
59
59
  "scripts": {
60
60
  "postinstall": "asc -f dts",