@aooth/arbac-moost 0.1.79 → 0.1.81
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.
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as AoothArbacClaims, t as ArbacUserProvider } from "../user.provider-
|
|
1
|
+
import { r as AoothArbacClaims, t as ArbacUserProvider } from "../user.provider-BZkeBV5s.mjs";
|
|
2
2
|
import { TAtscriptAnnotatedType, TAtscriptTypeObject, TMetadataMap, TValidatorOptions, Validator } from "@atscript/typescript/utils";
|
|
3
3
|
import { AoothUserCredentials } from "@aooth/user/atscript-db/model.as";
|
|
4
4
|
//#region src/atscript/auto-provider.d.ts
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { _ as ProjectionOf, a as conjoinArbacDbScopes, b as TArbacMeta, c as ArbacDbScope, d as extractUsedControlValues, f as applyAllowedFieldsAndSet, g as OwnFieldKey, h as NavTarget, i as ConjoinArbacDbScopesOptions, l as AsArbacDbController, m as NavRelationKey, n as ArbacUserProviderToken, o as RefForeignKey, p as ControlsOf, r as AoothArbacClaims, s as RefTableSource, t as ArbacUserProvider, u as enforceControlsPolicy, v as ARBAC_DELEGATED_AUTH, x as getArbacMate, y as ArbacDelegatedAuth } from "./user.provider-
|
|
1
|
+
import { _ as ProjectionOf, a as conjoinArbacDbScopes, b as TArbacMeta, c as ArbacDbScope, d as extractUsedControlValues, f as applyAllowedFieldsAndSet, g as OwnFieldKey, h as NavTarget, i as ConjoinArbacDbScopesOptions, l as AsArbacDbController, m as NavRelationKey, n as ArbacUserProviderToken, o as RefForeignKey, p as ControlsOf, r as AoothArbacClaims, s as RefTableSource, t as ArbacUserProvider, u as enforceControlsPolicy, v as ARBAC_DELEGATED_AUTH, x as getArbacMate, y as ArbacDelegatedAuth } from "./user.provider-BZkeBV5s.mjs";
|
|
2
2
|
import { Arbac, Arbac as Arbac$1, TArbacCompiledRule, TArbacEvalResult, TArbacEvalResult as TArbacEvalResult$1, TArbacRole, TArbacRoleForResource, TArbacRule, arbacPatternToRegex } from "@aooth/arbac-core";
|
|
3
|
-
import { ControlGate, TProjection, TScopeFieldRules, TScopeFilter, conjoinScopeFilters } from "@aooth/arbac";
|
|
3
|
+
import { ControlGate, ControlGate as ControlGate$1, TProjection, TScopeFieldRules, TScopeFilter, conjoinScopeFilters } from "@aooth/arbac";
|
|
4
4
|
import { TAuthGuardDef } from "@moostjs/event-http";
|
|
5
5
|
import { EventContext } from "@wooksjs/event-core";
|
|
6
6
|
import { FilterExpr, TDbRemoveGuardContext, TDbWriteCheckContext, TDbWriteGuardContext, TDeleteOptions, TMetaResponse, TRowResolveOptions, TWriteOptions, UniqueryControls } from "@atscript/db";
|
|
@@ -490,8 +490,13 @@ declare function pruneMetaByVisibility(meta: TMetaResponse, vis: MetaVisibility,
|
|
|
490
490
|
* still need `type` for their insert/update forms. PK + `preferredId` are
|
|
491
491
|
* never pruned.
|
|
492
492
|
*
|
|
493
|
+
* 3. The capabilities (`crud[op]` control lists, field `groupable` /
|
|
494
|
+
* `bucketable` / `numeric` / `sortable`, relation `filterable`, the
|
|
495
|
+
* aggregate and search envelope keys) follow each read op's `controls` policy
|
|
496
|
+
* ({@link gateMetaControls}).
|
|
497
|
+
*
|
|
493
498
|
* The search surface (`searchIndexes`, `searchable`, `vectorSearchable`,
|
|
494
|
-
* `geoSearchable`) is narrowed by moost-db itself (≥ 0.1.147, under the
|
|
499
|
+
* `geoSearchable`) is also narrowed by moost-db itself (≥ 0.1.147, under the
|
|
495
500
|
* overridden `hasField`), after this overlay.
|
|
496
501
|
*
|
|
497
502
|
* @param source - the controller's `this.readable` (identifiers, relations
|
package/dist/index.mjs
CHANGED
|
@@ -5,9 +5,8 @@ import { Authenticate, HttpError } from "@moostjs/event-http";
|
|
|
5
5
|
import { cached, cachedBy, current, eventTypeKey, key, tryGetCurrent } from "@wooksjs/event-core";
|
|
6
6
|
import { Inherit, Injectable, TInterceptorPriority, defineBeforeInterceptor, getConstructor, getMoostMate, useControllerContext, useLogger } from "moost";
|
|
7
7
|
import { computeInsights, containsRelationFilter, findAncestorInSet, getPath, hasRelationOp, isPlainObject, isRelationOp, translateQueryTree } from "@atscript/db";
|
|
8
|
-
import { AsDbController, AsDbReadableController, AsJsonValueHelpController, AsValueHelpController, DB_CRUD_HANDLERS, VALUE_HELP_CRUD_HANDLERS, badRequest, discoverRowLevelActions, getControllerFormType, getDbEndpoint, hasActionDelegations, resolveTerminalRef } from "@atscript/moost-db";
|
|
8
|
+
import { AsDbController, AsDbReadableController, AsJsonValueHelpController, AsValueHelpController, DB_CRUD_HANDLERS, VALUE_HELP_CRUD_HANDLERS, badRequest, discoverRowLevelActions, getControllerFormType, getDbEndpoint, hasActionDelegations, resolveTerminalRef, stableMeta } from "@atscript/moost-db";
|
|
9
9
|
import { groupByFields, walkFilter } from "@uniqu/core";
|
|
10
|
-
import { prerenderJson } from "@wooksjs/event-http";
|
|
11
10
|
//#region src/db/helpers.ts
|
|
12
11
|
/** `map.get(key)`, created with `create()` and stored on a miss. */
|
|
13
12
|
function getOrCreate(map, key, create) {
|
|
@@ -130,14 +129,14 @@ function identifierSet(table) {
|
|
|
130
129
|
function metaAlwaysVisibleFields(_controller, source) {
|
|
131
130
|
return identifierSet(source);
|
|
132
131
|
}
|
|
133
|
-
const NO_POLICY = {
|
|
132
|
+
const NO_POLICY$1 = {
|
|
134
133
|
json: NO_NAMES$1,
|
|
135
134
|
derived: /* @__PURE__ */ new Map(),
|
|
136
135
|
relations: void 0
|
|
137
136
|
};
|
|
138
137
|
const tablePolicyCache = /* @__PURE__ */ new WeakMap();
|
|
139
138
|
function tablePolicy(table) {
|
|
140
|
-
if (!table) return NO_POLICY;
|
|
139
|
+
if (!table) return NO_POLICY$1;
|
|
141
140
|
return getOrCreate(tablePolicyCache, table, () => {
|
|
142
141
|
const derived = /* @__PURE__ */ new Map();
|
|
143
142
|
for (const fd of table.fieldDescriptors ?? []) if (fd.derived?.sourcePath) derived.set(fd.path, fd.derived.sourcePath);
|
|
@@ -1242,15 +1241,25 @@ function metaDecisionKey(input, ctx) {
|
|
|
1242
1241
|
const vis = p.refTargets.get(type);
|
|
1243
1242
|
refTargets.push(idOf(type), ctx.isServed(type), vis ? encodeLevel(vis, NO_TREE, ctx, seen, 1) : 0);
|
|
1244
1243
|
}
|
|
1245
|
-
const { writable, nestedWrites
|
|
1246
|
-
|
|
1244
|
+
const { writable, nestedWrites } = p;
|
|
1245
|
+
const controls = [];
|
|
1246
|
+
for (const op of Object.keys(p.controls).toSorted()) {
|
|
1247
|
+
const policy = p.controls[op] ?? {};
|
|
1248
|
+
const gates = [op];
|
|
1249
|
+
for (const key of Object.keys(policy).toSorted()) {
|
|
1250
|
+
const gate = policy[key];
|
|
1251
|
+
if (typeof gate !== "boolean" && !Array.isArray(gate)) refuse();
|
|
1252
|
+
gates.push(key, Array.isArray(gate) ? sortedNames(gate) : gate);
|
|
1253
|
+
}
|
|
1254
|
+
controls.push(gates);
|
|
1255
|
+
}
|
|
1247
1256
|
return JSON.stringify([
|
|
1248
1257
|
bits(input.actions),
|
|
1249
1258
|
bits(input.crud),
|
|
1250
1259
|
encodeLevel(p.vis, p.tree, ctx, seen, 0),
|
|
1251
1260
|
writable === void 0 ? 0 : writable === "all" ? 1 : sortedNames(writable),
|
|
1252
1261
|
nestedWrites ? sortedNames(nestedWrites) : 0,
|
|
1253
|
-
|
|
1262
|
+
controls,
|
|
1254
1263
|
refTargets,
|
|
1255
1264
|
p.vis.table?.type ? ctx.isServed(p.vis.table.type) : 0
|
|
1256
1265
|
]);
|
|
@@ -1286,29 +1295,13 @@ function memoMetaProjection(envelope, key, max, compute) {
|
|
|
1286
1295
|
return out;
|
|
1287
1296
|
}
|
|
1288
1297
|
/**
|
|
1289
|
-
* Make a memoized `/meta` object shareable across requests:
|
|
1290
|
-
*
|
|
1291
|
-
*
|
|
1298
|
+
* Make a memoized `/meta` object shareable across requests: moost-db's
|
|
1299
|
+
* `stableMeta()` deep-freezes it and marks it identity-stable, so every later
|
|
1300
|
+
* `/meta` step memoizes on it and `GET /meta` serves it (or the step's
|
|
1301
|
+
* memoized output) serialized once, with a weak ETag.
|
|
1292
1302
|
*/
|
|
1293
1303
|
function shareMeta(obj) {
|
|
1294
|
-
|
|
1295
|
-
else Object.freeze(obj);
|
|
1296
|
-
return prerenderJson(obj, { etag: true });
|
|
1297
|
-
}
|
|
1298
|
-
let deepFreezeOn;
|
|
1299
|
-
function shouldDeepFreeze() {
|
|
1300
|
-
if (deepFreezeOn === void 0) {
|
|
1301
|
-
const env = typeof process === "undefined" ? void 0 : process.env.NODE_ENV;
|
|
1302
|
-
deepFreezeOn = env === "test" || env === "development";
|
|
1303
|
-
}
|
|
1304
|
-
return deepFreezeOn;
|
|
1305
|
-
}
|
|
1306
|
-
const deepFrozen = /* @__PURE__ */ new WeakSet();
|
|
1307
|
-
function deepFreeze(value) {
|
|
1308
|
-
if (value === null || typeof value !== "object" || deepFrozen.has(value)) return;
|
|
1309
|
-
deepFrozen.add(value);
|
|
1310
|
-
Object.freeze(value);
|
|
1311
|
-
for (const key of Object.keys(value)) deepFreeze(value[key]);
|
|
1304
|
+
return stableMeta(obj);
|
|
1312
1305
|
}
|
|
1313
1306
|
//#endregion
|
|
1314
1307
|
//#region src/db/meta-projection.ts
|
|
@@ -1856,8 +1849,13 @@ function crudHandlers(op, methods) {
|
|
|
1856
1849
|
* still need `type` for their insert/update forms. PK + `preferredId` are
|
|
1857
1850
|
* never pruned.
|
|
1858
1851
|
*
|
|
1852
|
+
* 3. The capabilities (`crud[op]` control lists, field `groupable` /
|
|
1853
|
+
* `bucketable` / `numeric` / `sortable`, relation `filterable`, the
|
|
1854
|
+
* aggregate and search envelope keys) follow each read op's `controls` policy
|
|
1855
|
+
* ({@link gateMetaControls}).
|
|
1856
|
+
*
|
|
1859
1857
|
* The search surface (`searchIndexes`, `searchable`, `vectorSearchable`,
|
|
1860
|
-
* `geoSearchable`) is narrowed by moost-db itself (≥ 0.1.147, under the
|
|
1858
|
+
* `geoSearchable`) is also narrowed by moost-db itself (≥ 0.1.147, under the
|
|
1861
1859
|
* overridden `hasField`), after this overlay.
|
|
1862
1860
|
*
|
|
1863
1861
|
* @param source - the controller's `this.readable` (identifiers, relations
|
|
@@ -1880,10 +1878,15 @@ async function applyArbacMetaOverlay(meta, source, _indexes) {
|
|
|
1880
1878
|
const crudBits = crudScopes.map((scopes) => scopes !== void 0);
|
|
1881
1879
|
const readScopes = [];
|
|
1882
1880
|
const writeScopes = [];
|
|
1881
|
+
const controls = {};
|
|
1883
1882
|
for (let i = 0; i < crudKeys.length; i++) {
|
|
1884
1883
|
const scopes = crudScopes[i];
|
|
1885
1884
|
if (!scopes) continue;
|
|
1886
|
-
(WRITE_CRUD_OPS.has(crudKeys[i])
|
|
1885
|
+
if (WRITE_CRUD_OPS.has(crudKeys[i])) writeScopes.push(...scopes);
|
|
1886
|
+
else {
|
|
1887
|
+
readScopes.push(...scopes);
|
|
1888
|
+
controls[crudKeys[i]] = effectiveScope(scopes).controls;
|
|
1889
|
+
}
|
|
1887
1890
|
}
|
|
1888
1891
|
const overlay = () => {
|
|
1889
1892
|
const crud = {};
|
|
@@ -1900,18 +1903,17 @@ async function applyArbacMetaOverlay(meta, source, _indexes) {
|
|
|
1900
1903
|
readScopes,
|
|
1901
1904
|
writeScopes
|
|
1902
1905
|
}, RECORD_CTX) : void 0, memoMax, overlay);
|
|
1903
|
-
const groupBy = effectiveScope(readScopes).controls.$groupBy;
|
|
1904
1906
|
const writable = collectWritableFields(writeScopes, false);
|
|
1905
1907
|
const nestedWrites = effectiveScope(writeScopes).nestedWrites;
|
|
1906
1908
|
if (isIdentifierSet(source)) {
|
|
1907
1909
|
const vis = buildScopeVisibility(readScopes, void 0, source);
|
|
1908
1910
|
const visible = Object.keys(vis.allowed).length > 0 || vis.withGrants.size > 0;
|
|
1909
1911
|
const overlaid = overlay();
|
|
1910
|
-
return
|
|
1912
|
+
return gateMetaControls(visible ? pruneMetaByVisibility(overlaid, {
|
|
1911
1913
|
...vis,
|
|
1912
1914
|
writable,
|
|
1913
1915
|
nestedWrites
|
|
1914
|
-
}) : overlaid,
|
|
1916
|
+
}) : overlaid, controls);
|
|
1915
1917
|
}
|
|
1916
1918
|
const vis = buildScopeVisibility(readScopes, source, { relations: await resolveRelationTree(readScopes, source, metaRelationNames(meta), { listSiblings: false }) });
|
|
1917
1919
|
const refTargets = await resolveRefTargets(vis, meta);
|
|
@@ -1925,14 +1927,14 @@ async function applyArbacMetaOverlay(meta, source, _indexes) {
|
|
|
1925
1927
|
tree: metaRelationNames(meta),
|
|
1926
1928
|
writable,
|
|
1927
1929
|
nestedWrites,
|
|
1928
|
-
|
|
1930
|
+
controls,
|
|
1929
1931
|
refTargets
|
|
1930
1932
|
}
|
|
1931
|
-
}, RECORD_CTX) : void 0, memoMax, () =>
|
|
1933
|
+
}, RECORD_CTX) : void 0, memoMax, () => gateMetaControls(pruneMetaByVisibility(overlay(), {
|
|
1932
1934
|
...vis,
|
|
1933
1935
|
writable,
|
|
1934
1936
|
nestedWrites
|
|
1935
|
-
}, refTargets),
|
|
1937
|
+
}, refTargets), controls));
|
|
1936
1938
|
}
|
|
1937
1939
|
/** What the `/meta` memo's decision record reads from the pruning rules. */
|
|
1938
1940
|
const RECORD_CTX = {
|
|
@@ -2095,28 +2097,143 @@ function stripFormTargets(runtime, node, readable, depth) {
|
|
|
2095
2097
|
type: changed
|
|
2096
2098
|
} : out;
|
|
2097
2099
|
}
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
*
|
|
2103
|
-
*
|
|
2104
|
-
*
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2100
|
+
const NO_POLICY = {};
|
|
2101
|
+
/** `crud[op]` entries that are query parts of their own, never seen by the controls gate. */
|
|
2102
|
+
const NOT_CONTROLS = /* @__PURE__ */ new Set(["filter", "insights"]);
|
|
2103
|
+
/**
|
|
2104
|
+
* Whether `control` is unusable in an op listing `list` under `policy`: its
|
|
2105
|
+
* gate is `false`, or everything it qualifies is unusable (`$index` /
|
|
2106
|
+
* `$fuzzy` / `$vector` name how `$search` runs, `$threshold` bounds
|
|
2107
|
+
* `$vector`, `$rowOrder` orders `first()` / `last()` in an aggregate
|
|
2108
|
+
* `$select`, `$nulls` places NULLs in `$sort` and `$rowOrder` keys).
|
|
2109
|
+
*/
|
|
2110
|
+
function isControlOff(control, list, policy) {
|
|
2111
|
+
if (!NOT_CONTROLS.has(control) && policy[`$${control}`] === false) return true;
|
|
2112
|
+
const off = (c) => isControlOff(c, list, policy);
|
|
2113
|
+
switch (control) {
|
|
2114
|
+
case "index":
|
|
2115
|
+
case "fuzzy": return list.includes("search") && off("search");
|
|
2116
|
+
case "vector": return off("search");
|
|
2117
|
+
case "threshold": return off("vector");
|
|
2118
|
+
case "rowOrder": return off("groupBy") || off("select");
|
|
2119
|
+
case "nulls": return off("sort") && (!list.includes("rowOrder") || off("rowOrder"));
|
|
2120
|
+
default: return false;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
/** Envelope keys that advertise aggregate queries (`$groupBy`, aggregate / bucket `$select`). */
|
|
2124
|
+
const AGGREGATE_ENVELOPE_KEYS = [
|
|
2125
|
+
"aggregateFns",
|
|
2126
|
+
"aggregateExpressions",
|
|
2127
|
+
"bucketUnits"
|
|
2128
|
+
];
|
|
2129
|
+
/**
|
|
2130
|
+
* The `/meta` capabilities follow each read op's `controls` policy (`policies`;
|
|
2131
|
+
* an op without one is unrestricted), mirroring the 403 `applyArbacControls`
|
|
2132
|
+
* raises on that op's requests:
|
|
2133
|
+
*
|
|
2134
|
+
* - `crud[op]` loses every control the op's policy makes unusable
|
|
2135
|
+
* ({@link isControlOff});
|
|
2136
|
+
* - a capability flag stays when SOME op listing its controls keeps them
|
|
2137
|
+
* (no op lists them → left as built): `groupable` (`groupBy`, and the
|
|
2138
|
+
* field on that op's `$groupBy` whitelist, if any), `bucketable` (the same
|
|
2139
|
+
* plus `select` — a bucket is checked as the field it buckets), `numeric`
|
|
2140
|
+
* and the aggregate envelope keys (`groupBy` + `select`: every aggregate,
|
|
2141
|
+
* calendar bucket or arithmetic `$select` is `$groupBy` use), `sortable`
|
|
2142
|
+
* (`sort`), `nullsPlacement` (`nulls`), `searchable` + `searchIndexes`
|
|
2143
|
+
* (`search`), `vectorSearchable` + the vector indexes (`search` +
|
|
2144
|
+
* `vector`), the non-default text indexes (`search` + `index`), a
|
|
2145
|
+
* relation's `filterable` (`filter` + `with`, and the relation on that
|
|
2146
|
+
* op's `$with` whitelist, if any).
|
|
2147
|
+
*
|
|
2148
|
+
* Returns `meta` itself when nothing changes.
|
|
2149
|
+
*
|
|
2150
|
+
* @since 0.1.75 (`groupable`), 0.1.80 (the rest)
|
|
2151
|
+
*/
|
|
2152
|
+
function gateMetaControls(meta, policies) {
|
|
2153
|
+
if (!Object.values(policies).some((p) => p && Object.keys(p).length > 0)) return meta;
|
|
2154
|
+
let changed = false;
|
|
2155
|
+
const crud = {};
|
|
2156
|
+
const ops = [];
|
|
2157
|
+
for (const [op, list] of Object.entries(meta.crud)) {
|
|
2158
|
+
const policy = policies[op] ?? NO_POLICY;
|
|
2159
|
+
const kept = list.filter((c) => !isControlOff(c, list, policy));
|
|
2160
|
+
if (kept.length !== list.length) changed = true;
|
|
2161
|
+
crud[op] = kept;
|
|
2162
|
+
ops.push({
|
|
2163
|
+
list,
|
|
2164
|
+
kept,
|
|
2165
|
+
policy
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
/** Some op listing `controls` keeps them all (and `name` passes its `whitelist` gate). */
|
|
2169
|
+
const served = (controls, name, whitelist = "$groupBy") => {
|
|
2170
|
+
const listing = ops.filter((o) => controls.every((c) => o.list.includes(c)));
|
|
2171
|
+
return listing.length === 0 || listing.some(({ kept, policy }) => {
|
|
2172
|
+
const gate = policy[whitelist];
|
|
2173
|
+
return controls.every((c) => kept.includes(c)) && (name === void 0 || !Array.isArray(gate) || gate.includes(name));
|
|
2174
|
+
});
|
|
2175
|
+
};
|
|
2176
|
+
const aggregates = served(["groupBy", "select"]);
|
|
2177
|
+
const sortable = served(["sort"]);
|
|
2111
2178
|
const fields = {};
|
|
2112
|
-
for (const [path, field] of entries
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2179
|
+
for (const [path, field] of Object.entries(meta.fields)) {
|
|
2180
|
+
let next = field;
|
|
2181
|
+
const strip = (flag, keep) => {
|
|
2182
|
+
if (!next[flag] || keep()) return;
|
|
2183
|
+
const { [flag]: _dropped, ...rest } = next;
|
|
2184
|
+
next = rest;
|
|
2185
|
+
};
|
|
2186
|
+
strip("groupable", () => served(["groupBy"], path));
|
|
2187
|
+
strip("bucketable", () => served(["groupBy", "select"], path));
|
|
2188
|
+
strip("numeric", () => aggregates);
|
|
2189
|
+
if (!sortable && next.sortable) next = {
|
|
2190
|
+
...next,
|
|
2191
|
+
sortable: false
|
|
2192
|
+
};
|
|
2193
|
+
if (next !== field) changed = true;
|
|
2194
|
+
fields[path] = next;
|
|
2195
|
+
}
|
|
2196
|
+
const out = {
|
|
2117
2197
|
...meta,
|
|
2118
|
-
fields
|
|
2198
|
+
fields,
|
|
2199
|
+
crud
|
|
2119
2200
|
};
|
|
2201
|
+
const unfilterable = (r) => r.filterable && !served(["filter", "with"], r.name, "$with");
|
|
2202
|
+
if (out.relations.some(unfilterable)) {
|
|
2203
|
+
out.relations = out.relations.map((r) => {
|
|
2204
|
+
if (!unfilterable(r)) return r;
|
|
2205
|
+
const { filterable: _filterable, ...rest } = r;
|
|
2206
|
+
return rest;
|
|
2207
|
+
});
|
|
2208
|
+
changed = true;
|
|
2209
|
+
}
|
|
2210
|
+
if (!aggregates) for (const key of AGGREGATE_ENVELOPE_KEYS) {
|
|
2211
|
+
if (out[key] === void 0) continue;
|
|
2212
|
+
delete out[key];
|
|
2213
|
+
changed = true;
|
|
2214
|
+
}
|
|
2215
|
+
if (out.nullsPlacement && !served(["nulls"])) {
|
|
2216
|
+
delete out.nullsPlacement;
|
|
2217
|
+
changed = true;
|
|
2218
|
+
}
|
|
2219
|
+
const search = served(["search"]);
|
|
2220
|
+
const vector = search && served(["search", "vector"]);
|
|
2221
|
+
const named = served(["search", "index"]);
|
|
2222
|
+
const all = out.searchIndexes ?? [];
|
|
2223
|
+
const indexes = all.filter((i) => i.type === "vector" ? vector : search && (named || !!i.isDefault));
|
|
2224
|
+
if (indexes.length !== all.length) {
|
|
2225
|
+
out.searchIndexes = indexes;
|
|
2226
|
+
changed = true;
|
|
2227
|
+
}
|
|
2228
|
+
if (out.searchable && !search) {
|
|
2229
|
+
out.searchable = false;
|
|
2230
|
+
changed = true;
|
|
2231
|
+
}
|
|
2232
|
+
if (out.vectorSearchable && !vector) {
|
|
2233
|
+
out.vectorSearchable = false;
|
|
2234
|
+
changed = true;
|
|
2235
|
+
}
|
|
2236
|
+
return changed ? out : meta;
|
|
2120
2237
|
}
|
|
2121
2238
|
const metaNamesCache = /* @__PURE__ */ new WeakMap();
|
|
2122
2239
|
/** The relation names `/meta`'s serialized type reaches: root relations, then nav props of each nav body. */
|
|
@@ -3264,14 +3381,19 @@ const preservedFieldsCache = /* @__PURE__ */ new WeakMap();
|
|
|
3264
3381
|
* - `readonly string[]`: allow only the listed values; reject any other.
|
|
3265
3382
|
*
|
|
3266
3383
|
* "Used" means the control key is present AND non-empty (an empty array is
|
|
3267
|
-
* treated as not used, matching how the parser leaves missing controls)
|
|
3384
|
+
* treated as not used, matching how the parser leaves missing controls) —
|
|
3385
|
+
* except `$groupBy`, whose presence alone marks an aggregate query: moost-db
|
|
3386
|
+
* (≥ 0.1.155) gives an ungrouped aggregate (`$select=sum(amount):total`, no
|
|
3387
|
+
* `$groupBy`) `$groupBy: []`, so a `false` gate denies it and a whitelist
|
|
3388
|
+
* passes it (no column is grouped on; the aggregated fields go through the
|
|
3389
|
+
* field-visibility gate like any other read).
|
|
3268
3390
|
*/
|
|
3269
3391
|
function enforceControlsPolicy(policy, controls) {
|
|
3270
3392
|
if (Object.keys(policy).length === 0) return;
|
|
3271
3393
|
for (const [key, gate] of Object.entries(policy)) {
|
|
3272
3394
|
const used = controls[key];
|
|
3273
3395
|
if (used === void 0 || used === null) continue;
|
|
3274
|
-
if (Array.isArray(used) && used.length === 0) continue;
|
|
3396
|
+
if (Array.isArray(used) && used.length === 0 && key !== "$groupBy") continue;
|
|
3275
3397
|
if (gate === false) throw new HttpError(403, `Control "${key}" is not allowed for your role`);
|
|
3276
3398
|
if (Array.isArray(gate)) {
|
|
3277
3399
|
const usedValues = extractUsedControlValues(key, used, controls);
|
|
@@ -84,20 +84,21 @@ type NavRelationKey<T> = keyof NavPropsOf<T> extends never ? string : (keyof Nav
|
|
|
84
84
|
* `unionProjections` / `restrictProjection` keep compiling.
|
|
85
85
|
*/
|
|
86
86
|
type ProjectionOf<T> = IsUnknown<T> extends true ? TProjection : Partial<Record<OwnFieldKey<T>, 0 | 1>>;
|
|
87
|
+
/** The Uniquery controls a gate can only allow or deny (`true` / `false`). */
|
|
88
|
+
type BooleanControl = "$select" | "$sort" | "$nulls" | "$skip" | "$limit" | "$count" | "$page" | "$size" | "$search" | "$index" | "$fuzzy" | "$vector" | "$threshold" | "$actions" | "$rowOrder" | "$having" | "$center" | "$maxDistance" | "$minDistance";
|
|
87
89
|
/**
|
|
88
|
-
* Typed control gates
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
90
|
+
* Typed control gates, matching what `unionControlsPolicy` accepts: only
|
|
91
|
+
* `$with` (nav relation names) and `$groupBy` (own field paths) take a
|
|
92
|
+
* whitelist; every other control is `true` / `false` (a whitelist there
|
|
93
|
+
* throws at scope merge). Unknown control keys are rejected. Falls back to
|
|
94
|
+
* the legacy untyped `Record<string, ControlGate>` for `T = unknown` so
|
|
95
|
+
* internal helpers (`unionControlsPolicy`) and existing untyped call sites
|
|
96
|
+
* keep compiling.
|
|
93
97
|
*/
|
|
94
98
|
type ControlsOf<T> = IsUnknown<T> extends true ? Record<string, ControlGate> : {
|
|
95
|
-
$with?:
|
|
96
|
-
$
|
|
97
|
-
|
|
98
|
-
$having?: ControlGate;
|
|
99
|
-
[key: `$${string}`]: ControlGate | Array<string> | undefined;
|
|
100
|
-
};
|
|
99
|
+
$with?: ReadonlyArray<NavRelationKey<T>> | boolean;
|
|
100
|
+
$groupBy?: ReadonlyArray<OwnFieldKey<T>> | boolean;
|
|
101
|
+
} & Partial<Record<BooleanControl, boolean>>;
|
|
101
102
|
//#endregion
|
|
102
103
|
//#region src/db/write-fields.d.ts
|
|
103
104
|
/**
|
|
@@ -173,6 +174,10 @@ interface ArbacDbScope<T = unknown> {
|
|
|
173
174
|
* `?$with=comments`, rejects `?$with=tasks`). Supported only for `$with`
|
|
174
175
|
* (relation names) and `$groupBy` (column names) in v1.
|
|
175
176
|
*
|
|
177
|
+
* An ungrouped aggregate (`$select=sum(amount):total`, no `$groupBy`) counts
|
|
178
|
+
* as `$groupBy` use (0.1.80+, with atscript-db 0.1.155+): `$groupBy: false`
|
|
179
|
+
* denies it; a `$groupBy` whitelist allows it (no column is grouped on).
|
|
180
|
+
*
|
|
176
181
|
* Across roles, gates union additively (silence wins) via {@link unionControlsPolicy}.
|
|
177
182
|
*
|
|
178
183
|
* @example `{ controls: { $with: false } }` — disable $with for this role.
|
|
@@ -406,7 +411,12 @@ declare class AsArbacDbController<T extends TAtscriptAnnotatedType = TAtscriptAn
|
|
|
406
411
|
* - `readonly string[]`: allow only the listed values; reject any other.
|
|
407
412
|
*
|
|
408
413
|
* "Used" means the control key is present AND non-empty (an empty array is
|
|
409
|
-
* treated as not used, matching how the parser leaves missing controls)
|
|
414
|
+
* treated as not used, matching how the parser leaves missing controls) —
|
|
415
|
+
* except `$groupBy`, whose presence alone marks an aggregate query: moost-db
|
|
416
|
+
* (≥ 0.1.155) gives an ungrouped aggregate (`$select=sum(amount):total`, no
|
|
417
|
+
* `$groupBy`) `$groupBy: []`, so a `false` gate denies it and a whitelist
|
|
418
|
+
* passes it (no column is grouped on; the aggregated fields go through the
|
|
419
|
+
* field-visibility gate like any other read).
|
|
410
420
|
*/
|
|
411
421
|
declare function enforceControlsPolicy(policy: Record<string, ControlGate>, controls: Record<string, unknown>): void;
|
|
412
422
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aooth/arbac-moost",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.81",
|
|
4
4
|
"description": "Moost RBAC integration for aoothjs (migrated from @moostjs/arbac)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abac",
|
|
@@ -61,30 +61,29 @@
|
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@uniqu/core": "^0.1.15",
|
|
64
|
-
"@aooth/arbac": "0.1.
|
|
65
|
-
"@aooth/
|
|
66
|
-
"@aooth/
|
|
64
|
+
"@aooth/arbac": "0.1.81",
|
|
65
|
+
"@aooth/user": "0.1.81",
|
|
66
|
+
"@aooth/arbac-core": "0.1.81"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@atscript/core": "^0.1.
|
|
70
|
-
"@atscript/db": "^0.1.
|
|
71
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
72
|
-
"@atscript/db-sqlite": "^0.1.
|
|
73
|
-
"@atscript/moost-db": "^0.1.
|
|
74
|
-
"@atscript/typescript": "^0.1.
|
|
75
|
-
"@moostjs/event-http": "^0.6.
|
|
69
|
+
"@atscript/core": "^0.1.106",
|
|
70
|
+
"@atscript/db": "^0.1.156",
|
|
71
|
+
"@atscript/db-sql-tools": "^0.1.156",
|
|
72
|
+
"@atscript/db-sqlite": "^0.1.156",
|
|
73
|
+
"@atscript/moost-db": "^0.1.156",
|
|
74
|
+
"@atscript/typescript": "^0.1.106",
|
|
75
|
+
"@moostjs/event-http": "^0.6.51",
|
|
76
76
|
"@types/better-sqlite3": "^7.6.13",
|
|
77
77
|
"better-sqlite3": "^12.6.2",
|
|
78
|
-
"unplugin-atscript": "^0.1.
|
|
78
|
+
"unplugin-atscript": "^0.1.106"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
|
-
"@atscript/db": "^0.1.
|
|
82
|
-
"@atscript/moost-db": "^0.1.
|
|
83
|
-
"@atscript/typescript": "^0.1.
|
|
84
|
-
"@moostjs/event-http": "^0.6.
|
|
85
|
-
"@wooksjs/event-core": "^0.7.
|
|
86
|
-
"
|
|
87
|
-
"moost": "^0.6.50"
|
|
81
|
+
"@atscript/db": "^0.1.156",
|
|
82
|
+
"@atscript/moost-db": "^0.1.156",
|
|
83
|
+
"@atscript/typescript": "^0.1.106",
|
|
84
|
+
"@moostjs/event-http": "^0.6.51",
|
|
85
|
+
"@wooksjs/event-core": "^0.7.29",
|
|
86
|
+
"moost": "^0.6.51"
|
|
88
87
|
},
|
|
89
88
|
"peerDependenciesMeta": {
|
|
90
89
|
"@atscript/db": {
|