@atscript/moost-db 0.1.154 → 0.1.156

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
@@ -1529,7 +1529,7 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
1529
1529
  const ignored = source.ignoredFields;
1530
1530
  for (const [path, entry] of flatMap) {
1531
1531
  if (!path || this._entries.has(path) || this._objectParents.has(path)) continue;
1532
- if (entry?.type?.kind !== "object") continue;
1532
+ if (!entry?.type || (0, _atscript_db.resolveDesignType)(entry) !== "object") continue;
1533
1533
  if (entry.metadata?.has?.("db.json")) continue;
1534
1534
  if (isNavOrDescendant(path)) continue;
1535
1535
  if (ignored.has(path)) continue;
@@ -1644,13 +1644,17 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
1644
1644
  * `predicate` is a filter entry's class (`collectQueryPaths` records it per
1645
1645
  * occurrence); it only matters for `op === "filter"` on a listed leaf.
1646
1646
  *
1647
+ * `nullTest` (since 0.1.155): the filter entry only tests presence
1648
+ * (`TFilterRef.nullTest`) — accepted on a nested-object parent whose leaves
1649
+ * are all visible.
1650
+ *
1647
1651
  * `prefix` (since 0.1.147) is this index's readable's dotted path from the
1648
1652
  * controller when it judges a relational predicate's operand (`"ticket."`):
1649
1653
  * `exists` still receives the LOCAL path, the verdict's `path` and message
1650
1654
  * name the prefixed one. A filter on a navigation path names the predicate
1651
1655
  * alternative (`ticket=$some(status=…)`).
1652
1656
  */
1653
- check(local, gateOp, exists, predicate = "compare", prefix = "") {
1657
+ check(local, gateOp, exists, predicate = "compare", prefix = "", nullTest = false) {
1654
1658
  const path = prefix + local;
1655
1659
  const requires = prefix === "" ? this._decorations.get(local) : void 0;
1656
1660
  if (requires) {
@@ -1707,6 +1711,7 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
1707
1711
  const all = this._objectParents.get(local);
1708
1712
  const leaves = all.filter(exists).map((leaf) => prefix + leaf);
1709
1713
  if (leaves.length === 0 && all.length > 0) return unknownField(path);
1714
+ if (op === "filter" && nullTest && all.length > 0 && all.every((leaf) => exists(leaf) && this._entries.get(leaf)?.filterBy.exists === void 0) && (0, _atscript_db.findAncestorInSet)(local, this._bucketTable.jsonValueParents) === void 0) return;
1710
1715
  return {
1711
1716
  path,
1712
1717
  message: `"${path}" is a nested object — filter or sort on one of its leaves (${leafHint(leaves)})`
@@ -1847,7 +1852,7 @@ var RelationPredicateGate = class {
1847
1852
  const index = this._indexOf(target);
1848
1853
  const visible = (p) => this._visibleAt(target, prefix, p);
1849
1854
  for (const ref of refs.filter) {
1850
- const error = ref.predicate === "relation" ? this.checkRef(ref, state, target, prefix, depth) : verdictError(index.check(ref.path, "filter", visible, ref.predicate, prefix));
1855
+ const error = ref.predicate === "relation" ? this.checkRef(ref, state, target, prefix, depth) : verdictError(index.check(ref.path, "filter", visible, ref.predicate, prefix, ref.nullTest));
1851
1856
  if (error) return error;
1852
1857
  }
1853
1858
  }
@@ -2872,7 +2877,7 @@ let AsReadableController = _AsReadableController = class AsReadableController {
2872
2877
  *
2873
2878
  * Annotation whitelist: keeps `meta.*`, `expect.*`, `db.rel.*`, the `db.*`
2874
2879
  * keys the shared db validator plugin reads in db-client (`db.json`,
2875
- * `db.patch.strategy`, `db.default*`, `db.column.version`,
2880
+ * `db.patch.strategy`, `db.default*`, `db.onUpdate.now`, `db.column.version`,
2876
2881
  * `db.column.derived`) and the client-facing `db.http.path` /
2877
2882
  * `db.writeOnly` / `db.sort.nulls`; strips every other `db.*` (table, column, index, etc.).
2878
2883
  * Override in subclass to customise — keep the validator keys, or client
@@ -2886,7 +2891,7 @@ let AsReadableController = _AsReadableController = class AsReadableController {
2886
2891
  key,
2887
2892
  value
2888
2893
  };
2889
- if (key === "db.json" || key === "db.patch.strategy" || key.startsWith("db.default") || key === "db.http.path" || key === "db.writeOnly" || key === "db.column.version" || key === "db.column.derived" || key === "db.sort.nulls") return {
2894
+ if (key === "db.json" || key === "db.patch.strategy" || key.startsWith("db.default") || key === "db.onUpdate.now" || key === "db.http.path" || key === "db.writeOnly" || key === "db.column.version" || key === "db.column.derived" || key === "db.sort.nulls") return {
2890
2895
  key,
2891
2896
  value
2892
2897
  };
@@ -5246,7 +5251,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
5246
5251
  if (relError) return relError;
5247
5252
  continue;
5248
5253
  }
5249
- const verdict = capabilities.check(ref.path, "filter", isVisible, ref.predicate);
5254
+ const verdict = capabilities.check(ref.path, "filter", isVisible, ref.predicate, "", ref.nullTest);
5250
5255
  if (verdict) return badRequest(verdict.path, verdict.message);
5251
5256
  }
5252
5257
  const liveWith = parsed.controls?.$with;
@@ -5323,7 +5328,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
5323
5328
  }
5324
5329
  }
5325
5330
  /**
5326
- * {@link checkComputedSelect} (before the controls DTO), the controls DTO
5331
+ * {@link checkComputedSelect} (before the controls DTO), on `/pages` the
5332
+ * aggregate 400 (since 0.1.155), the controls DTO
5327
5333
  * ({@link validateControls}), then the `$with` relation names at every
5328
5334
  * level — BEFORE the `$with` sub-query paths ({@link validateInsights}),
5329
5335
  * so a hidden or nonexistent nested relation answers `Unknown relation`,
@@ -5334,6 +5340,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
5334
5340
  const computedError = this.checkComputedSelect(parsed.controls);
5335
5341
  if (computedError) return computedError;
5336
5342
  const controls = parsed.controls;
5343
+ if (type === "pages" && (controls?.$groupBy !== void 0 || computedMode(parsed))) return badRequest(controls?.$groupBy === void 0 ? "$select" : "$groupBy", "Aggregate queries are only valid on /query");
5337
5344
  if (controls && typeof controls === "object") this._clientWith.set(controls, snapshotClientWith(controls.$with) ?? null);
5338
5345
  const controlsError = this.validateControls(controls, type);
5339
5346
  if (controlsError) return new _moostjs_event_http.HttpError(400, controlsError);
@@ -7012,13 +7019,15 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
7012
7019
  */
7013
7020
  async query(url) {
7014
7021
  const { parsed, controls } = await this.parseRequest("query", url);
7015
- const groupBy = controls.$groupBy;
7016
- if (groupBy?.length && controls.$with?.length) return new _moostjs_event_http.HttpError(400, "Cannot combine $with and $groupBy in the same query");
7017
- if (groupBy?.length && controls.$vector !== void 0) return new _moostjs_event_http.HttpError(400, "Cannot combine $vector and $groupBy in the same query");
7022
+ const computed = computedMode(parsed);
7023
+ if (computed && controls.$groupBy === void 0) controls.$groupBy = [];
7024
+ const aggregate = computed || !!controls.$groupBy?.length;
7025
+ if (aggregate && controls.$with?.length) return new _moostjs_event_http.HttpError(400, "Cannot combine $with and $groupBy in the same query");
7026
+ if (aggregate && controls.$vector !== void 0) return new _moostjs_event_http.HttpError(400, "Cannot combine $vector and $groupBy in the same query");
7018
7027
  const error = this.validateParsed(parsed, "query");
7019
7028
  if (error) return error;
7020
7029
  let gateRefs;
7021
- if (groupBy?.length && this._writeOnlySet.size > 0) {
7030
+ if (aggregate && this._writeOnlySet.size > 0) {
7022
7031
  gateRefs = (0, _atscript_db.collectQueryPaths)(parsed, computedMode(parsed));
7023
7032
  const refs = gateRefs.aggregateMode ? gateRefs : (0, _atscript_db.collectQueryPaths)(parsed, true);
7024
7033
  const sealed = [
@@ -7033,7 +7042,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
7033
7042
  const gateError = gateRefs ? this._checkCapabilitiesWith(parsed, gateRefs) : this.checkCapabilities(parsed);
7034
7043
  if (gateError) return gateError;
7035
7044
  const clientFilter = await this._relationOverlay(parsed);
7036
- if (groupBy?.length) {
7045
+ if (aggregate) {
7037
7046
  const filter = this.applySearchFallback(await this.transformFilter(clientFilter), controls);
7038
7047
  return this.readable.aggregate({
7039
7048
  filter,
package/dist/index.d.cts CHANGED
@@ -241,7 +241,7 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
241
241
  *
242
242
  * Annotation whitelist: keeps `meta.*`, `expect.*`, `db.rel.*`, the `db.*`
243
243
  * keys the shared db validator plugin reads in db-client (`db.json`,
244
- * `db.patch.strategy`, `db.default*`, `db.column.version`,
244
+ * `db.patch.strategy`, `db.default*`, `db.onUpdate.now`, `db.column.version`,
245
245
  * `db.column.derived`) and the client-facing `db.http.path` /
246
246
  * `db.writeOnly` / `db.sort.nulls`; strips every other `db.*` (table, column, index, etc.).
247
247
  * Override in subclass to customise — keep the validator keys, or client
@@ -1064,13 +1064,17 @@ declare class FieldCapabilityIndex implements TQueryPathSource {
1064
1064
  * `predicate` is a filter entry's class (`collectQueryPaths` records it per
1065
1065
  * occurrence); it only matters for `op === "filter"` on a listed leaf.
1066
1066
  *
1067
+ * `nullTest` (since 0.1.155): the filter entry only tests presence
1068
+ * (`TFilterRef.nullTest`) — accepted on a nested-object parent whose leaves
1069
+ * are all visible.
1070
+ *
1067
1071
  * `prefix` (since 0.1.147) is this index's readable's dotted path from the
1068
1072
  * controller when it judges a relational predicate's operand (`"ticket."`):
1069
1073
  * `exists` still receives the LOCAL path, the verdict's `path` and message
1070
1074
  * name the prefixed one. A filter on a navigation path names the predicate
1071
1075
  * alternative (`ticket=$some(status=…)`).
1072
1076
  */
1073
- check(local: string, gateOp: TGateOp, exists: (path: string) => boolean, predicate?: TFilterPredicate, prefix?: string): TCapabilityVerdict | undefined;
1077
+ check(local: string, gateOp: TGateOp, exists: (path: string) => boolean, predicate?: TFilterPredicate, prefix?: string, nullTest?: boolean): TCapabilityVerdict | undefined;
1074
1078
  }
1075
1079
  //#endregion
1076
1080
  //#region src/as-db-readable.controller.d.ts
@@ -1376,7 +1380,8 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
1376
1380
  */
1377
1381
  protected validateInsights(insights: Map<string, unknown>): string | undefined;
1378
1382
  /**
1379
- * {@link checkComputedSelect} (before the controls DTO), the controls DTO
1383
+ * {@link checkComputedSelect} (before the controls DTO), on `/pages` the
1384
+ * aggregate 400 (since 0.1.155), the controls DTO
1380
1385
  * ({@link validateControls}), then the `$with` relation names at every
1381
1386
  * level — BEFORE the `$with` sub-query paths ({@link validateInsights}),
1382
1387
  * so a hidden or nonexistent nested relation answers `Unknown relation`,
package/dist/index.d.mts CHANGED
@@ -241,7 +241,7 @@ declare abstract class AsReadableController<T extends TAtscriptAnnotatedType = T
241
241
  *
242
242
  * Annotation whitelist: keeps `meta.*`, `expect.*`, `db.rel.*`, the `db.*`
243
243
  * keys the shared db validator plugin reads in db-client (`db.json`,
244
- * `db.patch.strategy`, `db.default*`, `db.column.version`,
244
+ * `db.patch.strategy`, `db.default*`, `db.onUpdate.now`, `db.column.version`,
245
245
  * `db.column.derived`) and the client-facing `db.http.path` /
246
246
  * `db.writeOnly` / `db.sort.nulls`; strips every other `db.*` (table, column, index, etc.).
247
247
  * Override in subclass to customise — keep the validator keys, or client
@@ -1064,13 +1064,17 @@ declare class FieldCapabilityIndex implements TQueryPathSource {
1064
1064
  * `predicate` is a filter entry's class (`collectQueryPaths` records it per
1065
1065
  * occurrence); it only matters for `op === "filter"` on a listed leaf.
1066
1066
  *
1067
+ * `nullTest` (since 0.1.155): the filter entry only tests presence
1068
+ * (`TFilterRef.nullTest`) — accepted on a nested-object parent whose leaves
1069
+ * are all visible.
1070
+ *
1067
1071
  * `prefix` (since 0.1.147) is this index's readable's dotted path from the
1068
1072
  * controller when it judges a relational predicate's operand (`"ticket."`):
1069
1073
  * `exists` still receives the LOCAL path, the verdict's `path` and message
1070
1074
  * name the prefixed one. A filter on a navigation path names the predicate
1071
1075
  * alternative (`ticket=$some(status=…)`).
1072
1076
  */
1073
- check(local: string, gateOp: TGateOp, exists: (path: string) => boolean, predicate?: TFilterPredicate, prefix?: string): TCapabilityVerdict | undefined;
1077
+ check(local: string, gateOp: TGateOp, exists: (path: string) => boolean, predicate?: TFilterPredicate, prefix?: string, nullTest?: boolean): TCapabilityVerdict | undefined;
1074
1078
  }
1075
1079
  //#endregion
1076
1080
  //#region src/as-db-readable.controller.d.ts
@@ -1376,7 +1380,8 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
1376
1380
  */
1377
1381
  protected validateInsights(insights: Map<string, unknown>): string | undefined;
1378
1382
  /**
1379
- * {@link checkComputedSelect} (before the controls DTO), the controls DTO
1383
+ * {@link checkComputedSelect} (before the controls DTO), on `/pages` the
1384
+ * aggregate 400 (since 0.1.155), the controls DTO
1380
1385
  * ({@link validateControls}), then the `$with` relation names at every
1381
1386
  * level — BEFORE the `$with` sub-query paths ({@link validateInsights}),
1382
1387
  * so a hidden or nonexistent nested relation answers `Unknown relation`,
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import { ValidatorError, defineAnnotatedType, isAnnotatedType, serializeAnnotate
3
3
  import { Body, Delete, Get, HttpError, MoostHttp, Patch, Post, Put, Query, Url, httpKind, prerenderJson } from "@moostjs/event-http";
4
4
  import { ApplyDecorators, Controller, Inherit, Inject, Intercept, Moost, Optional, Param, Pipe, Provide, Resolve, TInterceptorPriority, TPipePriority, current, defineBeforeInterceptor, defineInterceptor, definePipeFn, getMoostMate, useControllerContext, withControllerContext } from "moost";
5
5
  import { parseUrl } from "@uniqu/url";
6
- import { ADAPTER_FILTER_REASON, ALL_AGGREGATE_FNS, DbError, ENCRYPTED_REASON, acceptedOperatorsHint, aliasTargetOf, andFilters, bucketSourceVerdict, canFilterLeaf, checkHavingKeys, classifyQueryPath, collectQueryPaths, collectQueryPaths as collectQueryPaths$1, containsRelationFilter, findAncestorInSet, geoIndexNotFoundMessage, groupSourceVerdict, hasRelationOp, isEmptyObject, isJsonValueField, isPlainObject, isRelationOp, narrowerFilterOps, normalizeComputedSelect, numericOperandProblem, reconcileCas, searchIndexNotFoundMessage, searchMemberKind, selfOrAncestor, unsupportedOperatorMessage, vectorIndexNotFoundMessage } from "@atscript/db";
6
+ import { ADAPTER_FILTER_REASON, ALL_AGGREGATE_FNS, DbError, ENCRYPTED_REASON, acceptedOperatorsHint, aliasTargetOf, andFilters, bucketSourceVerdict, canFilterLeaf, checkHavingKeys, classifyQueryPath, collectQueryPaths, collectQueryPaths as collectQueryPaths$1, containsRelationFilter, findAncestorInSet, geoIndexNotFoundMessage, groupSourceVerdict, hasRelationOp, isEmptyObject, isJsonValueField, isPlainObject, isRelationOp, narrowerFilterOps, normalizeComputedSelect, numericOperandProblem, reconcileCas, resolveDesignType, searchIndexNotFoundMessage, searchMemberKind, selfOrAncestor, unsupportedOperatorMessage, vectorIndexNotFoundMessage } from "@atscript/db";
7
7
  import { cached, current as current$1, defineWook, key, routeParamsKey, useRouteParams } from "@wooksjs/event-core";
8
8
  import { useBody } from "@wooksjs/http-body";
9
9
  import { BUCKET_UNITS } from "@uniqu/core";
@@ -1528,7 +1528,7 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
1528
1528
  const ignored = source.ignoredFields;
1529
1529
  for (const [path, entry] of flatMap) {
1530
1530
  if (!path || this._entries.has(path) || this._objectParents.has(path)) continue;
1531
- if (entry?.type?.kind !== "object") continue;
1531
+ if (!entry?.type || resolveDesignType(entry) !== "object") continue;
1532
1532
  if (entry.metadata?.has?.("db.json")) continue;
1533
1533
  if (isNavOrDescendant(path)) continue;
1534
1534
  if (ignored.has(path)) continue;
@@ -1643,13 +1643,17 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
1643
1643
  * `predicate` is a filter entry's class (`collectQueryPaths` records it per
1644
1644
  * occurrence); it only matters for `op === "filter"` on a listed leaf.
1645
1645
  *
1646
+ * `nullTest` (since 0.1.155): the filter entry only tests presence
1647
+ * (`TFilterRef.nullTest`) — accepted on a nested-object parent whose leaves
1648
+ * are all visible.
1649
+ *
1646
1650
  * `prefix` (since 0.1.147) is this index's readable's dotted path from the
1647
1651
  * controller when it judges a relational predicate's operand (`"ticket."`):
1648
1652
  * `exists` still receives the LOCAL path, the verdict's `path` and message
1649
1653
  * name the prefixed one. A filter on a navigation path names the predicate
1650
1654
  * alternative (`ticket=$some(status=…)`).
1651
1655
  */
1652
- check(local, gateOp, exists, predicate = "compare", prefix = "") {
1656
+ check(local, gateOp, exists, predicate = "compare", prefix = "", nullTest = false) {
1653
1657
  const path = prefix + local;
1654
1658
  const requires = prefix === "" ? this._decorations.get(local) : void 0;
1655
1659
  if (requires) {
@@ -1706,6 +1710,7 @@ var FieldCapabilityIndex = class FieldCapabilityIndex {
1706
1710
  const all = this._objectParents.get(local);
1707
1711
  const leaves = all.filter(exists).map((leaf) => prefix + leaf);
1708
1712
  if (leaves.length === 0 && all.length > 0) return unknownField(path);
1713
+ if (op === "filter" && nullTest && all.length > 0 && all.every((leaf) => exists(leaf) && this._entries.get(leaf)?.filterBy.exists === void 0) && findAncestorInSet(local, this._bucketTable.jsonValueParents) === void 0) return;
1709
1714
  return {
1710
1715
  path,
1711
1716
  message: `"${path}" is a nested object — filter or sort on one of its leaves (${leafHint(leaves)})`
@@ -1846,7 +1851,7 @@ var RelationPredicateGate = class {
1846
1851
  const index = this._indexOf(target);
1847
1852
  const visible = (p) => this._visibleAt(target, prefix, p);
1848
1853
  for (const ref of refs.filter) {
1849
- const error = ref.predicate === "relation" ? this.checkRef(ref, state, target, prefix, depth) : verdictError(index.check(ref.path, "filter", visible, ref.predicate, prefix));
1854
+ const error = ref.predicate === "relation" ? this.checkRef(ref, state, target, prefix, depth) : verdictError(index.check(ref.path, "filter", visible, ref.predicate, prefix, ref.nullTest));
1850
1855
  if (error) return error;
1851
1856
  }
1852
1857
  }
@@ -2871,7 +2876,7 @@ let AsReadableController = _AsReadableController = class AsReadableController {
2871
2876
  *
2872
2877
  * Annotation whitelist: keeps `meta.*`, `expect.*`, `db.rel.*`, the `db.*`
2873
2878
  * keys the shared db validator plugin reads in db-client (`db.json`,
2874
- * `db.patch.strategy`, `db.default*`, `db.column.version`,
2879
+ * `db.patch.strategy`, `db.default*`, `db.onUpdate.now`, `db.column.version`,
2875
2880
  * `db.column.derived`) and the client-facing `db.http.path` /
2876
2881
  * `db.writeOnly` / `db.sort.nulls`; strips every other `db.*` (table, column, index, etc.).
2877
2882
  * Override in subclass to customise — keep the validator keys, or client
@@ -2885,7 +2890,7 @@ let AsReadableController = _AsReadableController = class AsReadableController {
2885
2890
  key,
2886
2891
  value
2887
2892
  };
2888
- if (key === "db.json" || key === "db.patch.strategy" || key.startsWith("db.default") || key === "db.http.path" || key === "db.writeOnly" || key === "db.column.version" || key === "db.column.derived" || key === "db.sort.nulls") return {
2893
+ if (key === "db.json" || key === "db.patch.strategy" || key.startsWith("db.default") || key === "db.onUpdate.now" || key === "db.http.path" || key === "db.writeOnly" || key === "db.column.version" || key === "db.column.derived" || key === "db.sort.nulls") return {
2889
2894
  key,
2890
2895
  value
2891
2896
  };
@@ -5243,7 +5248,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
5243
5248
  if (relError) return relError;
5244
5249
  continue;
5245
5250
  }
5246
- const verdict = capabilities.check(ref.path, "filter", isVisible, ref.predicate);
5251
+ const verdict = capabilities.check(ref.path, "filter", isVisible, ref.predicate, "", ref.nullTest);
5247
5252
  if (verdict) return badRequest(verdict.path, verdict.message);
5248
5253
  }
5249
5254
  const liveWith = parsed.controls?.$with;
@@ -5320,7 +5325,8 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
5320
5325
  }
5321
5326
  }
5322
5327
  /**
5323
- * {@link checkComputedSelect} (before the controls DTO), the controls DTO
5328
+ * {@link checkComputedSelect} (before the controls DTO), on `/pages` the
5329
+ * aggregate 400 (since 0.1.155), the controls DTO
5324
5330
  * ({@link validateControls}), then the `$with` relation names at every
5325
5331
  * level — BEFORE the `$with` sub-query paths ({@link validateInsights}),
5326
5332
  * so a hidden or nonexistent nested relation answers `Unknown relation`,
@@ -5331,6 +5337,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
5331
5337
  const computedError = this.checkComputedSelect(parsed.controls);
5332
5338
  if (computedError) return computedError;
5333
5339
  const controls = parsed.controls;
5340
+ if (type === "pages" && (controls?.$groupBy !== void 0 || computedMode(parsed))) return badRequest(controls?.$groupBy === void 0 ? "$select" : "$groupBy", "Aggregate queries are only valid on /query");
5334
5341
  if (controls && typeof controls === "object") this._clientWith.set(controls, snapshotClientWith(controls.$with) ?? null);
5335
5342
  const controlsError = this.validateControls(controls, type);
5336
5343
  if (controlsError) return new HttpError(400, controlsError);
@@ -7009,13 +7016,15 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
7009
7016
  */
7010
7017
  async query(url) {
7011
7018
  const { parsed, controls } = await this.parseRequest("query", url);
7012
- const groupBy = controls.$groupBy;
7013
- if (groupBy?.length && controls.$with?.length) return new HttpError(400, "Cannot combine $with and $groupBy in the same query");
7014
- if (groupBy?.length && controls.$vector !== void 0) return new HttpError(400, "Cannot combine $vector and $groupBy in the same query");
7019
+ const computed = computedMode(parsed);
7020
+ if (computed && controls.$groupBy === void 0) controls.$groupBy = [];
7021
+ const aggregate = computed || !!controls.$groupBy?.length;
7022
+ if (aggregate && controls.$with?.length) return new HttpError(400, "Cannot combine $with and $groupBy in the same query");
7023
+ if (aggregate && controls.$vector !== void 0) return new HttpError(400, "Cannot combine $vector and $groupBy in the same query");
7015
7024
  const error = this.validateParsed(parsed, "query");
7016
7025
  if (error) return error;
7017
7026
  let gateRefs;
7018
- if (groupBy?.length && this._writeOnlySet.size > 0) {
7027
+ if (aggregate && this._writeOnlySet.size > 0) {
7019
7028
  gateRefs = collectQueryPaths$1(parsed, computedMode(parsed));
7020
7029
  const refs = gateRefs.aggregateMode ? gateRefs : collectQueryPaths$1(parsed, true);
7021
7030
  const sealed = [
@@ -7030,7 +7039,7 @@ let AsDbReadableController = _AsDbReadableController = class AsDbReadableControl
7030
7039
  const gateError = gateRefs ? this._checkCapabilitiesWith(parsed, gateRefs) : this.checkCapabilities(parsed);
7031
7040
  if (gateError) return gateError;
7032
7041
  const clientFilter = await this._relationOverlay(parsed);
7033
- if (groupBy?.length) {
7042
+ if (aggregate) {
7034
7043
  const filter = this.applySearchFallback(await this.transformFilter(clientFilter), controls);
7035
7044
  return this.readable.aggregate({
7036
7045
  filter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/moost-db",
3
- "version": "0.1.154",
3
+ "version": "0.1.156",
4
4
  "description": "Generic database controller for Moost with Atscript.",
5
5
  "keywords": [
6
6
  "annotations",
@@ -44,29 +44,29 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@uniqu/url": "^0.1.15",
47
- "@atscript/db-memory": "^0.1.154"
47
+ "@atscript/db-memory": "^0.1.156"
48
48
  },
49
49
  "devDependencies": {
50
- "@atscript/core": "^0.1.103",
51
- "@atscript/typescript": "^0.1.103",
52
- "@moostjs/event-http": "^0.6.49",
50
+ "@atscript/core": "^0.1.106",
51
+ "@atscript/typescript": "^0.1.106",
52
+ "@moostjs/event-http": "^0.6.51",
53
53
  "@uniqu/core": "^0.1.15",
54
- "@wooksjs/event-core": "^0.7.28",
55
- "@wooksjs/event-http": "^0.7.28",
56
- "@wooksjs/http-body": "^0.7.28",
54
+ "@wooksjs/event-core": "^0.7.29",
55
+ "@wooksjs/event-http": "^0.7.29",
56
+ "@wooksjs/http-body": "^0.7.29",
57
57
  "better-sqlite3": "^12.6.2",
58
- "moost": "^0.6.49",
59
- "unplugin-atscript": "^0.1.103",
60
- "@atscript/db-sqlite": "^0.1.154"
58
+ "moost": "^0.6.51",
59
+ "unplugin-atscript": "^0.1.106",
60
+ "@atscript/db-sqlite": "^0.1.156"
61
61
  },
62
62
  "peerDependencies": {
63
- "@atscript/typescript": "^0.1.103",
64
- "@moostjs/event-http": "^0.6.49",
63
+ "@atscript/typescript": "^0.1.106",
64
+ "@moostjs/event-http": "^0.6.51",
65
65
  "@uniqu/core": "^0.1.15",
66
- "@wooksjs/event-core": "^0.7.28",
67
- "@wooksjs/http-body": "^0.7.28",
68
- "moost": "^0.6.49",
69
- "@atscript/db": "^0.1.154"
66
+ "@wooksjs/event-core": "^0.7.29",
67
+ "@wooksjs/http-body": "^0.7.29",
68
+ "moost": "^0.6.51",
69
+ "@atscript/db": "^0.1.156"
70
70
  },
71
71
  "scripts": {
72
72
  "postinstall": "asc -f dts",