@aooth/arbac-moost 0.1.74 → 0.1.76
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-D0ySI_-A.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,11 +1,11 @@
|
|
|
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-D0ySI_-A.mjs";
|
|
2
2
|
import { Arbac, Arbac as Arbac$1, TArbacCompiledRule, TArbacEvalResult, TArbacEvalResult as TArbacEvalResult$1, TArbacRole, TArbacRoleForResource, TArbacRule, arbacPatternToRegex } from "@aooth/arbac-core";
|
|
3
3
|
import { ControlGate, 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";
|
|
7
7
|
import { AsDbReadableController, AsJsonValueHelpController, AsValueHelpController, TDbActionScopeContext, TDbControlsType, TDbIndexFieldPaths, TDbRequestContext, ValueHelpSelect } from "@atscript/moost-db";
|
|
8
|
-
import { TAtscriptAnnotatedType, TAtscriptDataType } from "@atscript/typescript/utils";
|
|
8
|
+
import { TAtscriptAnnotatedType, TAtscriptDataType, TSerializedAnnotatedType } from "@atscript/typescript/utils";
|
|
9
9
|
//#region src/db/visibility.d.ts
|
|
10
10
|
/**
|
|
11
11
|
* The slice of an atscript-db readable (table / view) the visibility walk
|
|
@@ -311,6 +311,12 @@ declare class AsArbacDbReadableController<T extends TAtscriptAnnotatedType = TAt
|
|
|
311
311
|
protected validateControls(controls: Record<string, unknown>, type: TDbControlsType): string | undefined;
|
|
312
312
|
/** Same contract as {@link AsArbacDbController.applyMetaOverlay}. */
|
|
313
313
|
protected applyMetaOverlay(meta: TMetaResponse): Promise<TMetaResponse>;
|
|
314
|
+
/**
|
|
315
|
+
* `GET /meta/form/:name` under the `/meta` overlay's pointer rule: a form
|
|
316
|
+
* prop's `ref` / `ui.valueHelp` binding to a table the caller cannot read
|
|
317
|
+
* (or whose named fields they cannot see) is stripped.
|
|
318
|
+
*/
|
|
319
|
+
metaForm(name: string): Promise<TSerializedAnnotatedType>;
|
|
314
320
|
/** Same contract as {@link AsArbacDbController.authorizeForm}. */
|
|
315
321
|
protected authorizeForm(_name: string, actionNames: readonly string[]): Promise<boolean>;
|
|
316
322
|
/** Same contract as {@link AsArbacDbController.actionRowScope}. */
|
|
@@ -359,6 +365,8 @@ declare class AsArbacDbReadableController<T extends TAtscriptAnnotatedType = TAt
|
|
|
359
365
|
*/
|
|
360
366
|
declare abstract class AsArbacValueHelpController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>> extends AsValueHelpController<T, DataType> {
|
|
361
367
|
private _arbacHost?;
|
|
368
|
+
/** Makes this controller the binding / `ref` policy source for its bound type's ARBAC resource. */
|
|
369
|
+
protected readonly _arbacValueHelpTarget: true;
|
|
362
370
|
private get arbacHost();
|
|
363
371
|
protected prepareRequest(_ctx: TDbRequestContext): Promise<void>;
|
|
364
372
|
/** Same contract as {@link AsArbacDbController.transformFilter}. */
|
|
@@ -392,6 +400,8 @@ declare abstract class AsArbacValueHelpController<T extends TAtscriptAnnotatedTy
|
|
|
392
400
|
*/
|
|
393
401
|
declare class AsArbacJsonValueHelpController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>> extends AsJsonValueHelpController<T, DataType> {
|
|
394
402
|
private _arbacHost?;
|
|
403
|
+
/** Makes this controller the binding / `ref` policy source for its bound type's ARBAC resource. */
|
|
404
|
+
protected readonly _arbacValueHelpTarget: true;
|
|
395
405
|
private get arbacHost();
|
|
396
406
|
protected prepareRequest(_ctx: TDbRequestContext): Promise<void>;
|
|
397
407
|
/** Same contract as {@link AsArbacDbController.transformFilter}. */
|
|
@@ -703,9 +713,14 @@ interface ArbacWriteTable {
|
|
|
703
713
|
* - `checkRefs` — every enforced FK the write sets must reference rows the
|
|
704
714
|
* caller can read on the target table (→ 403);
|
|
705
715
|
* - WITH CHECK when the adapter cannot roll back — validated here, before
|
|
706
|
-
* the write (see {@link checkArbacWrite} for the transactional path)
|
|
716
|
+
* the write (see {@link checkArbacWrite} for the transactional path);
|
|
717
|
+
* - WITH CHECK of an insert, even on a transactional adapter, evaluated on
|
|
718
|
+
* the submitted rows before the write when the in-memory evaluator can
|
|
719
|
+
* model it (a forbidden row → 403, never a duplicate-key 409; since
|
|
720
|
+
* 0.1.75) — and, with `ignoreConflicts` (`?$onConflict=ignore`), refused
|
|
721
|
+
* whenever it cannot, since the post-write check never sees skipped rows.
|
|
707
722
|
*/
|
|
708
|
-
declare function guardArbacWrite(ctx: TDbWriteGuardContext, scopes: readonly ArbacDbScope[], table: ArbacWriteTable, refs: RefTableSource): Promise<void>;
|
|
723
|
+
declare function guardArbacWrite(ctx: TDbWriteGuardContext, scopes: readonly ArbacDbScope[], table: ArbacWriteTable, refs: RefTableSource, ignoreConflicts?: boolean): Promise<void>;
|
|
709
724
|
/**
|
|
710
725
|
* WITH CHECK after the write, inside its transaction: every written row must
|
|
711
726
|
* match the scopes' `check` (default `filter`) — else 403 and the write rolls
|
|
@@ -773,11 +788,16 @@ interface ArbacDbScopeHelpers<T = unknown> {
|
|
|
773
788
|
* enforcement the ARBAC DB controllers' CRUD writes get — nested writes
|
|
774
789
|
* (`nestedWrites`, 403), USING (pre-image in scope, 404), `checkRefs` (403)
|
|
775
790
|
* and WITH CHECK (403, rolled back). `allowedFields` / `set` are not applied
|
|
776
|
-
* — overlay {@link set} yourself.
|
|
791
|
+
* — overlay {@link set} yourself. For `insertMany(rows, { onConflict: "ignore" })`
|
|
792
|
+
* pass the same `{ onConflict: "ignore" }`: conflicting rows are skipped, so
|
|
793
|
+
* a check that cannot be evaluated before the write refuses the batch
|
|
794
|
+
* (since 0.1.75).
|
|
777
795
|
*
|
|
778
796
|
* @since 0.1.72
|
|
779
797
|
*/
|
|
780
|
-
writeOptions<Row extends object = Record<string, unknown>>(table: ArbacGuardedTable
|
|
798
|
+
writeOptions<Row extends object = Record<string, unknown>>(table: ArbacGuardedTable, opts?: {
|
|
799
|
+
onConflict?: "ignore";
|
|
800
|
+
}): TWriteOptions<Row>;
|
|
781
801
|
/**
|
|
782
802
|
* `TDeleteOptions` for `table.deleteOne` in a handler: the id is pinned
|
|
783
803
|
* among in-scope rows and the row must match the scope filter (else 404) —
|
package/dist/index.mjs
CHANGED
|
@@ -4,8 +4,8 @@ import { DB_SCOPE_KEYS, DENY_FILTER, ScopeFieldConfigError, applyScopeFieldFilte
|
|
|
4
4
|
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
|
-
import { computeInsights, containsRelationFilter, findAncestorInSet, getPath, hasRelationOp, isPlainObject, isRelationOp } from "@atscript/db";
|
|
8
|
-
import { AsDbController, AsDbReadableController, AsJsonValueHelpController, AsValueHelpController, DB_CRUD_HANDLERS, VALUE_HELP_CRUD_HANDLERS, badRequest, discoverRowLevelActions, getDbEndpoint, hasActionDelegations, resolveTerminalRef } from "@atscript/moost-db";
|
|
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";
|
|
9
9
|
import { groupByFields, walkFilter } from "@uniqu/core";
|
|
10
10
|
//#region src/db/helpers.ts
|
|
11
11
|
/** `map.get(key)`, created with `create()` and stored on a miss. */
|
|
@@ -298,11 +298,28 @@ const registry = /* @__PURE__ */ new WeakMap();
|
|
|
298
298
|
*/
|
|
299
299
|
function registerArbacDbTarget(controller) {
|
|
300
300
|
const c = controller;
|
|
301
|
-
const target = c.readable?.type ?? c.readable;
|
|
301
|
+
const target = valueHelpTargets.get(controller)?.source.type ?? c.readable?.type ?? c.readable;
|
|
302
302
|
if (!target) return true;
|
|
303
303
|
getOrCreate(getOrCreate(registry, c.app ?? NO_APP, () => /* @__PURE__ */ new WeakMap()), target, () => /* @__PURE__ */ new Map()).set(getConstructor(controller), controller);
|
|
304
304
|
return true;
|
|
305
305
|
}
|
|
306
|
+
/** Value-help controllers have no `readable`: their visibility source and read handler method. */
|
|
307
|
+
const valueHelpTargets = /* @__PURE__ */ new WeakMap();
|
|
308
|
+
/**
|
|
309
|
+
* Register an ARBAC value-help controller as an owner of its bound type's
|
|
310
|
+
* ARBAC resource, so a `ui.valueHelp` binding (or `ref`) from another
|
|
311
|
+
* controller's `/meta` is kept for a caller who can read the dictionary
|
|
312
|
+
* through it (its `runQuery` grant, evaluated against `source`).
|
|
313
|
+
*
|
|
314
|
+
* @returns `true` (so it can initialize a field)
|
|
315
|
+
*/
|
|
316
|
+
function registerArbacValueHelpTarget(controller, source) {
|
|
317
|
+
valueHelpTargets.set(controller, {
|
|
318
|
+
source,
|
|
319
|
+
method: "runQuery"
|
|
320
|
+
});
|
|
321
|
+
return registerArbacDbTarget(controller);
|
|
322
|
+
}
|
|
306
323
|
function registeredControllers(target) {
|
|
307
324
|
let app = NO_APP;
|
|
308
325
|
try {
|
|
@@ -346,13 +363,14 @@ function evaluateHandlerOutcomes(ids) {
|
|
|
346
363
|
async function evaluateTypeRead(type) {
|
|
347
364
|
const controllers = registeredControllers(type);
|
|
348
365
|
if (controllers.length === 0) return null;
|
|
366
|
+
const tableOf = (c) => valueHelpTargets.get(c)?.source ?? c.readable;
|
|
349
367
|
const scopes = await evaluateHandlers(controllers.map((c) => ({
|
|
350
|
-
...resolveHandlerArbacIds(c, "query"),
|
|
351
|
-
table: c
|
|
368
|
+
...resolveHandlerArbacIds(c, valueHelpTargets.get(c)?.method ?? "query"),
|
|
369
|
+
table: tableOf(c)
|
|
352
370
|
})));
|
|
353
371
|
return scopes ? {
|
|
354
372
|
scopes,
|
|
355
|
-
table: controllers[0]
|
|
373
|
+
table: tableOf(controllers[0])
|
|
356
374
|
} : null;
|
|
357
375
|
}
|
|
358
376
|
/**
|
|
@@ -1115,7 +1133,7 @@ function pruneMetaByVisibility(meta, vis, refTargets) {
|
|
|
1115
1133
|
...meta,
|
|
1116
1134
|
fields,
|
|
1117
1135
|
relations,
|
|
1118
|
-
type: pruneSerializedType(meta.type, {
|
|
1136
|
+
type: pruneSerializedType(inlineTypeRefs(meta.type), {
|
|
1119
1137
|
basePath: "",
|
|
1120
1138
|
vis,
|
|
1121
1139
|
relationNames,
|
|
@@ -1125,6 +1143,153 @@ function pruneMetaByVisibility(meta, vis, refTargets) {
|
|
|
1125
1143
|
if (out.versionColumn !== void 0 && !isMetaFieldVisible(out.versionColumn, vis)) delete out.versionColumn;
|
|
1126
1144
|
return out;
|
|
1127
1145
|
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Replace every `$ref` entry with a copy of the body serialized under its `id`
|
|
1148
|
+
* (the first node carrying that id), so each use is pruned by ITS OWN path's
|
|
1149
|
+
* visibility. A shared body pruned once would resolve, on the client, to the
|
|
1150
|
+
* first use's shape at every other use: hidden fields leak through a later
|
|
1151
|
+
* `$ref`, and a hidden first use leaves later `$ref`s dangling. An `own`
|
|
1152
|
+
* entry keeps its own metadata / optional / `ref`. Unknown ids and recursive
|
|
1153
|
+
* back-references stay `$ref`s (a recursive one points at an ancestor that is
|
|
1154
|
+
* itself retained). Never mutates the (cached) input.
|
|
1155
|
+
*/
|
|
1156
|
+
function inlineTypeRefs(root) {
|
|
1157
|
+
return getOrCreate(inlinedCache, root, () => {
|
|
1158
|
+
const bodies = /* @__PURE__ */ new Map();
|
|
1159
|
+
collectBodies(root, bodies);
|
|
1160
|
+
if (bodies.size === 0) return root;
|
|
1161
|
+
return inlineNode(root, {
|
|
1162
|
+
bodies,
|
|
1163
|
+
budget: 2e4,
|
|
1164
|
+
n: 0
|
|
1165
|
+
}, /* @__PURE__ */ new Map(), false) ?? root;
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
const inlinedCache = /* @__PURE__ */ new WeakMap();
|
|
1169
|
+
function childNodes(node) {
|
|
1170
|
+
const def = node.type;
|
|
1171
|
+
const out = [];
|
|
1172
|
+
if (def.kind === "object") {
|
|
1173
|
+
out.push(...Object.values(def.props));
|
|
1174
|
+
for (const p of def.propsPatterns ?? []) out.push(p.def);
|
|
1175
|
+
} else if (def.kind === "array") out.push(def.of);
|
|
1176
|
+
else if (Array.isArray(def.items)) out.push(...def.items);
|
|
1177
|
+
const refType = node.ref?.type;
|
|
1178
|
+
if (refType && refType.type) out.push(refType);
|
|
1179
|
+
return out;
|
|
1180
|
+
}
|
|
1181
|
+
function collectBodies(node, bodies) {
|
|
1182
|
+
if (node.type.kind === "$ref") return;
|
|
1183
|
+
if (node.id !== void 0 && !bodies.has(node.id)) bodies.set(node.id, node);
|
|
1184
|
+
for (const child of childNodes(node)) collectBodies(child, bodies);
|
|
1185
|
+
}
|
|
1186
|
+
function inlineNode(node, ctx, stack, inlining) {
|
|
1187
|
+
const def = node.type;
|
|
1188
|
+
if (def.kind === "$ref") {
|
|
1189
|
+
const ancestor = stack.get(def.id);
|
|
1190
|
+
if (ancestor !== void 0) return {
|
|
1191
|
+
...node,
|
|
1192
|
+
type: {
|
|
1193
|
+
...def,
|
|
1194
|
+
id: ancestor
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
const body = ctx.bodies.get(def.id);
|
|
1198
|
+
if (!body) return node;
|
|
1199
|
+
const inlined = inlineNode(body, ctx, stack, true);
|
|
1200
|
+
if (!inlined) return void 0;
|
|
1201
|
+
if (!def.own) return inlined;
|
|
1202
|
+
const { ref: _ref, optional: _optional, ...rest } = inlined;
|
|
1203
|
+
return {
|
|
1204
|
+
...rest,
|
|
1205
|
+
metadata: node.metadata,
|
|
1206
|
+
...node.optional === void 0 ? {} : { optional: node.optional },
|
|
1207
|
+
...node.ref ? { ref: node.ref } : {}
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
if (inlining && --ctx.budget < 0) return void 0;
|
|
1211
|
+
let nextStack = stack;
|
|
1212
|
+
let out = node;
|
|
1213
|
+
if (node.id !== void 0) {
|
|
1214
|
+
const copyId = `${node.id}#${++ctx.n}`;
|
|
1215
|
+
nextStack = new Map(stack).set(node.id, copyId);
|
|
1216
|
+
out = {
|
|
1217
|
+
...node,
|
|
1218
|
+
id: copyId
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
const d = def;
|
|
1222
|
+
if (d.kind === "object") {
|
|
1223
|
+
const props = {};
|
|
1224
|
+
for (const [k, v] of Object.entries(d.props)) {
|
|
1225
|
+
const r = inlineNode(v, ctx, nextStack, inlining);
|
|
1226
|
+
if (r) props[k] = r;
|
|
1227
|
+
}
|
|
1228
|
+
const propsPatterns = [];
|
|
1229
|
+
for (const p of d.propsPatterns ?? []) {
|
|
1230
|
+
const r = inlineNode(p.def, ctx, nextStack, inlining);
|
|
1231
|
+
if (r) propsPatterns.push({
|
|
1232
|
+
...p,
|
|
1233
|
+
def: r
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
out = {
|
|
1237
|
+
...out,
|
|
1238
|
+
type: {
|
|
1239
|
+
...def,
|
|
1240
|
+
props,
|
|
1241
|
+
propsPatterns
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
} else if (d.kind === "array") {
|
|
1245
|
+
const of = inlineNode(d.of, ctx, nextStack, inlining) ?? emptyStub();
|
|
1246
|
+
out = {
|
|
1247
|
+
...out,
|
|
1248
|
+
type: {
|
|
1249
|
+
...def,
|
|
1250
|
+
of
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
} else if (Array.isArray(d.items)) {
|
|
1254
|
+
const items = d.items.map((i) => inlineNode(i, ctx, nextStack, inlining) ?? emptyStub());
|
|
1255
|
+
out = {
|
|
1256
|
+
...out,
|
|
1257
|
+
type: {
|
|
1258
|
+
...def,
|
|
1259
|
+
items
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
const refType = out.ref?.type;
|
|
1264
|
+
if (out.ref && refType && refType.type) {
|
|
1265
|
+
const r = inlineNode(refType, ctx, nextStack, inlining);
|
|
1266
|
+
if (r) out = {
|
|
1267
|
+
...out,
|
|
1268
|
+
ref: {
|
|
1269
|
+
...out.ref,
|
|
1270
|
+
type: r
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
else {
|
|
1274
|
+
const { ref: _r, ...noRef } = out;
|
|
1275
|
+
out = noRef;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return out;
|
|
1279
|
+
}
|
|
1280
|
+
/** A body-less placeholder for a positional child dropped by the inline budget. */
|
|
1281
|
+
function emptyStub() {
|
|
1282
|
+
return {
|
|
1283
|
+
$v: 2,
|
|
1284
|
+
type: {
|
|
1285
|
+
kind: "object",
|
|
1286
|
+
props: {},
|
|
1287
|
+
propsPatterns: [],
|
|
1288
|
+
tags: []
|
|
1289
|
+
},
|
|
1290
|
+
metadata: {}
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1128
1293
|
function pruneSerializedType(node, level) {
|
|
1129
1294
|
const { basePath, vis, relationNames, refTargets } = level;
|
|
1130
1295
|
const def = node.type;
|
|
@@ -1151,20 +1316,21 @@ function pruneSerializedType(node, level) {
|
|
|
1151
1316
|
basePath: path
|
|
1152
1317
|
};
|
|
1153
1318
|
if (!isMetaFieldVisible(path, vis)) {
|
|
1154
|
-
if (isPathWritable(path, vis)) props[name] = stampWriteOnly(prop);
|
|
1155
|
-
else if (hasWritableDescendant(path, vis.writable)) props[name] = stampWriteOnly(pruneSerializedType(prop, child));
|
|
1319
|
+
if (isPathWritable(path, vis)) props[name] = withVisibleTargets(stampWriteOnly(prop), path, vis, refTargets, level.inBranch);
|
|
1320
|
+
else if (hasWritableDescendant(path, vis.writable)) props[name] = withVisibleTargets(stampWriteOnly(pruneSerializedType(prop, child)), path, vis, refTargets, level.inBranch);
|
|
1156
1321
|
continue;
|
|
1157
1322
|
}
|
|
1158
|
-
|
|
1159
|
-
props[name] = pruned.ref && !isRefVisible(path, vis, refTargets) ? withoutRef(pruned) : pruned;
|
|
1323
|
+
props[name] = withVisibleTargets(pruneSerializedType(prop, child), path, vis, refTargets, level.inBranch);
|
|
1160
1324
|
}
|
|
1161
|
-
|
|
1325
|
+
const pruned = {
|
|
1162
1326
|
...node,
|
|
1163
1327
|
type: {
|
|
1164
1328
|
...def,
|
|
1165
1329
|
props
|
|
1166
1330
|
}
|
|
1167
1331
|
};
|
|
1332
|
+
if (basePath !== "" || !def.propsPatterns?.length || !vis.table?.flatMap || !refTargets) return pruned;
|
|
1333
|
+
return gateChildren(pruned, level.inBranch ? void 0 : vis.table?.type, vis, refTargets, false);
|
|
1168
1334
|
}
|
|
1169
1335
|
if (def.kind === "array") return {
|
|
1170
1336
|
...node,
|
|
@@ -1177,7 +1343,10 @@ function pruneSerializedType(node, level) {
|
|
|
1177
1343
|
...node,
|
|
1178
1344
|
type: {
|
|
1179
1345
|
...def,
|
|
1180
|
-
items: def.items.map((item) => pruneSerializedType(item,
|
|
1346
|
+
items: def.items.map((item) => pruneSerializedType(item, {
|
|
1347
|
+
...level,
|
|
1348
|
+
inBranch: true
|
|
1349
|
+
}))
|
|
1181
1350
|
}
|
|
1182
1351
|
};
|
|
1183
1352
|
return node;
|
|
@@ -1202,36 +1371,202 @@ function withoutRef(node) {
|
|
|
1202
1371
|
const { ref: _ref, ...rest } = node;
|
|
1203
1372
|
return rest;
|
|
1204
1373
|
}
|
|
1374
|
+
/** The `/meta` annotation key of a dictionary value-help binding (`@ui.valueHelp`, since 0.1.75); a string constant — arbac-moost does not import `@atscript/ui`. */
|
|
1375
|
+
const UI_VALUE_HELP = "ui.valueHelp";
|
|
1376
|
+
function withoutBinding(node) {
|
|
1377
|
+
const { [UI_VALUE_HELP]: _binding, ...metadata } = node.metadata;
|
|
1378
|
+
return {
|
|
1379
|
+
...node,
|
|
1380
|
+
metadata
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
const bindingCache = /* @__PURE__ */ new WeakMap();
|
|
1384
|
+
/**
|
|
1385
|
+
* The runtime `ui.valueHelp` binding of a flat-map entry — the dictionary and
|
|
1386
|
+
* the fields it names there: the committed `field` plus every field its
|
|
1387
|
+
* static filter reads. `undefined` = no binding; `null` = a binding that
|
|
1388
|
+
* cannot be resolved (callers fail closed and strip it).
|
|
1389
|
+
*/
|
|
1390
|
+
function bindingOf(entry) {
|
|
1391
|
+
const raw = entry?.metadata?.get?.(UI_VALUE_HELP);
|
|
1392
|
+
if (!raw) return void 0;
|
|
1393
|
+
let binding = bindingCache.get(entry);
|
|
1394
|
+
if (binding === void 0) {
|
|
1395
|
+
binding = resolveBinding(raw);
|
|
1396
|
+
bindingCache.set(entry, binding);
|
|
1397
|
+
}
|
|
1398
|
+
return binding;
|
|
1399
|
+
}
|
|
1400
|
+
function resolveBinding(raw) {
|
|
1401
|
+
try {
|
|
1402
|
+
const t = raw.target;
|
|
1403
|
+
const type = typeof t === "function" ? t() : t?.type();
|
|
1404
|
+
if (!type) return null;
|
|
1405
|
+
const fields = typeof raw.field === "string" ? [raw.field] : [];
|
|
1406
|
+
if (raw.filter) translateQueryTree(raw.filter, (ref) => {
|
|
1407
|
+
fields.push(ref.field);
|
|
1408
|
+
return ref.field;
|
|
1409
|
+
});
|
|
1410
|
+
return {
|
|
1411
|
+
type,
|
|
1412
|
+
fields
|
|
1413
|
+
};
|
|
1414
|
+
} catch {
|
|
1415
|
+
return null;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
/** Every table a flat-map entry points at: its `ref` target and its value-help binding. */
|
|
1419
|
+
function targetsOf(entry) {
|
|
1420
|
+
return [refTargetOf(entry), bindingOf(entry)].filter((t) => !!t);
|
|
1421
|
+
}
|
|
1205
1422
|
/** The field a runtime prop's `ref` finally points at (the same terminal `/meta` serializes). */
|
|
1206
1423
|
function refTargetOf(entry) {
|
|
1207
1424
|
const def = entry;
|
|
1208
1425
|
if (!def?.ref) return void 0;
|
|
1209
1426
|
const terminal = resolveTerminalRef(def);
|
|
1210
|
-
if (terminal) return
|
|
1427
|
+
if (terminal) return {
|
|
1428
|
+
type: terminal.type,
|
|
1429
|
+
fields: terminal.field ? [terminal.field] : []
|
|
1430
|
+
};
|
|
1211
1431
|
try {
|
|
1432
|
+
const { type, field } = def.ref;
|
|
1212
1433
|
return {
|
|
1213
|
-
type:
|
|
1214
|
-
field:
|
|
1434
|
+
type: type(),
|
|
1435
|
+
fields: field ? [field] : []
|
|
1215
1436
|
};
|
|
1216
1437
|
} catch {
|
|
1217
1438
|
return;
|
|
1218
1439
|
}
|
|
1219
1440
|
}
|
|
1441
|
+
/** A table-served type: any `db.*` annotation (`db.table`, `db.view`, `db.http.path`, …). */
|
|
1442
|
+
function isDbServed(type) {
|
|
1443
|
+
const meta = type.metadata;
|
|
1444
|
+
if (!meta || typeof meta.keys !== "function") return false;
|
|
1445
|
+
for (const key of meta.keys()) if (key.startsWith("db.")) return true;
|
|
1446
|
+
return false;
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* A target survives when the caller can read its table AND see every field it
|
|
1450
|
+
* names: a self-target against this level's visibility, a foreign one against
|
|
1451
|
+
* the caller's visibility on that table (`refTargets`; unreadable → hidden). A
|
|
1452
|
+
* target naming no field (a prop typed as a table interface, `@db.json snap?:
|
|
1453
|
+
* Dict`) still needs the table readable, unless the type is not table-served.
|
|
1454
|
+
*/
|
|
1455
|
+
function isTargetVisible(target, vis, refTargets) {
|
|
1456
|
+
if (target.fields.length === 0 && !isDbServed(target.type)) return true;
|
|
1457
|
+
const targetVis = target.type === vis.table?.type ? vis : refTargets.get(target.type);
|
|
1458
|
+
return !!targetVis && target.fields.every((f) => isMetaFieldVisible(f, targetVis));
|
|
1459
|
+
}
|
|
1460
|
+
/** The binding on the TYPE a `ref` points at (an alias such as `@ui.valueHelp X type Code = string`). */
|
|
1461
|
+
function refTypeBindingOf(entry) {
|
|
1462
|
+
try {
|
|
1463
|
+
const type = entry?.ref?.type?.();
|
|
1464
|
+
return type ? bindingOf(type) : null;
|
|
1465
|
+
} catch {
|
|
1466
|
+
return null;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
/** Both targets of one node: stripped = hidden. The serialized `ref.type` carries its own metadata. */
|
|
1470
|
+
function gateOwn(node, entry, visible) {
|
|
1471
|
+
let out = node;
|
|
1472
|
+
if (out.ref) {
|
|
1473
|
+
const ref = refTargetOf(entry);
|
|
1474
|
+
if (!ref || !visible(ref)) out = withoutRef(out);
|
|
1475
|
+
}
|
|
1476
|
+
if (out.metadata?.[UI_VALUE_HELP] !== void 0) {
|
|
1477
|
+
const binding = bindingOf(entry);
|
|
1478
|
+
if (!binding || !visible(binding)) out = withoutBinding(out);
|
|
1479
|
+
}
|
|
1480
|
+
const refMeta = out.ref?.type?.metadata;
|
|
1481
|
+
if (out.ref && refMeta?.[UI_VALUE_HELP] !== void 0) {
|
|
1482
|
+
const binding = refTypeBindingOf(entry);
|
|
1483
|
+
if (!binding || !visible(binding)) {
|
|
1484
|
+
const { [UI_VALUE_HELP]: _b, ...metadata } = refMeta;
|
|
1485
|
+
out = {
|
|
1486
|
+
...out,
|
|
1487
|
+
ref: {
|
|
1488
|
+
...out.ref,
|
|
1489
|
+
type: {
|
|
1490
|
+
...out.ref.type,
|
|
1491
|
+
metadata
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return out;
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Gate the children of `node` a prop walk does not reach by flat path: array
|
|
1501
|
+
* elements, union / intersection / tuple members and wildcard props — each
|
|
1502
|
+
* paired with its runtime counterpart under `runtime` (a missing one fails
|
|
1503
|
+
* closed). `deep` also descends object props (own-field subtrees only; a nav
|
|
1504
|
+
* prop belongs to its own level).
|
|
1505
|
+
*/
|
|
1506
|
+
function gateChildren(node, runtime, vis, refTargets, deep) {
|
|
1507
|
+
const visible = (t) => isTargetVisible(t, vis, refTargets);
|
|
1508
|
+
const gate = (child, rt) => gateChildren(gateOwn(child, rt, visible), rt, vis, refTargets, true);
|
|
1509
|
+
const rtType = runtime?.type;
|
|
1510
|
+
const def = node.type;
|
|
1511
|
+
if (def.kind === "object") {
|
|
1512
|
+
let next = def;
|
|
1513
|
+
if (deep && def.props) {
|
|
1514
|
+
const props = {};
|
|
1515
|
+
for (const [name, prop] of Object.entries(def.props)) props[name] = isNavProp(prop) ? prop : gate(prop, rtType?.props?.get?.(name));
|
|
1516
|
+
next = {
|
|
1517
|
+
...next,
|
|
1518
|
+
props
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
if (def.propsPatterns?.length) {
|
|
1522
|
+
const patterns = def.propsPatterns.map((p) => {
|
|
1523
|
+
const rt = rtType?.propsPatterns?.find((r) => r.pattern?.source === p.pattern?.source && r.pattern?.flags === p.pattern?.flags)?.def;
|
|
1524
|
+
return {
|
|
1525
|
+
...p,
|
|
1526
|
+
def: gate(p.def, rt)
|
|
1527
|
+
};
|
|
1528
|
+
});
|
|
1529
|
+
next = {
|
|
1530
|
+
...next,
|
|
1531
|
+
propsPatterns: patterns
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
return next === def ? node : {
|
|
1535
|
+
...node,
|
|
1536
|
+
type: next
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
if (def.kind === "array") return {
|
|
1540
|
+
...node,
|
|
1541
|
+
type: {
|
|
1542
|
+
...def,
|
|
1543
|
+
of: gate(def.of, rtType?.of)
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
if (Array.isArray(def.items)) {
|
|
1547
|
+
const items = def.items.map((item, i) => gate(item, rtType?.items?.[i]));
|
|
1548
|
+
return {
|
|
1549
|
+
...node,
|
|
1550
|
+
type: {
|
|
1551
|
+
...def,
|
|
1552
|
+
items
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
return node;
|
|
1557
|
+
}
|
|
1220
1558
|
/**
|
|
1221
|
-
*
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1559
|
+
* Strip a serialized `ref` / `ui.valueHelp` binding at `path` — on the prop
|
|
1560
|
+
* itself, on a `ref` target's own metadata, and on every array element, union
|
|
1561
|
+
* / intersection / tuple member and wildcard prop below it — whose target is
|
|
1562
|
+
* hidden from the caller. Without runtime schema info (`vis.table.flatMap`) or
|
|
1563
|
+
* `refTargets`, all are kept.
|
|
1226
1564
|
*/
|
|
1227
|
-
function
|
|
1228
|
-
const
|
|
1229
|
-
if (!
|
|
1230
|
-
const
|
|
1231
|
-
|
|
1232
|
-
if (target.type === table.type) return isMetaFieldVisible(target.field, vis);
|
|
1233
|
-
const targetVis = refTargets.get(target.type);
|
|
1234
|
-
return !!targetVis && isMetaFieldVisible(target.field, targetVis);
|
|
1565
|
+
function withVisibleTargets(node, path, vis, refTargets, ambiguous = false) {
|
|
1566
|
+
const flatMap = vis.table?.flatMap;
|
|
1567
|
+
if (!flatMap || !refTargets || !serializedHasTarget(node)) return node;
|
|
1568
|
+
const entry = ambiguous ? void 0 : flatMap.get(path);
|
|
1569
|
+
return gateChildren(gateOwn(node, entry, (t) => isTargetVisible(t, vis, refTargets)), entry, vis, refTargets, false);
|
|
1235
1570
|
}
|
|
1236
1571
|
const NO_NAMES = /* @__PURE__ */ new Set();
|
|
1237
1572
|
/** A nav prop inside a joined type: atscript-db stamps `db.rel.to` / `from` / `via`. */
|
|
@@ -1313,23 +1648,203 @@ async function applyArbacMetaOverlay(meta, source, _indexes) {
|
|
|
1313
1648
|
crud
|
|
1314
1649
|
};
|
|
1315
1650
|
if (readScopes.length === 0) return overlaid;
|
|
1651
|
+
const groupBy = effectiveScope(readScopes).controls.$groupBy;
|
|
1316
1652
|
const writable = collectWritableFields(writeScopes, false);
|
|
1317
1653
|
const nestedWrites = effectiveScope(writeScopes).nestedWrites;
|
|
1318
1654
|
if (isIdentifierSet(source)) {
|
|
1319
1655
|
const vis = buildScopeVisibility(readScopes, void 0, source);
|
|
1320
|
-
return Object.keys(vis.allowed).length > 0 || vis.withGrants.size > 0 ? pruneMetaByVisibility(overlaid, {
|
|
1656
|
+
return gateGroupable(Object.keys(vis.allowed).length > 0 || vis.withGrants.size > 0 ? pruneMetaByVisibility(overlaid, {
|
|
1321
1657
|
...vis,
|
|
1322
1658
|
writable,
|
|
1323
1659
|
nestedWrites
|
|
1324
|
-
}) : overlaid;
|
|
1660
|
+
}) : overlaid, groupBy);
|
|
1325
1661
|
}
|
|
1326
1662
|
const vis = buildScopeVisibility(readScopes, source, { relations: await resolveRelationTree(readScopes, source, metaRelationNames(meta), { listSiblings: false }) });
|
|
1327
1663
|
const refTargets = await resolveRefTargets(vis, meta);
|
|
1328
|
-
return pruneMetaByVisibility(overlaid, {
|
|
1664
|
+
return gateGroupable(pruneMetaByVisibility(overlaid, {
|
|
1329
1665
|
...vis,
|
|
1330
1666
|
writable,
|
|
1331
1667
|
nestedWrites
|
|
1332
|
-
}, refTargets);
|
|
1668
|
+
}, refTargets), groupBy);
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* The `/meta/form/:name` overlay: a form is a plain annotated type (no table),
|
|
1672
|
+
* so only its cross-table pointers are projected. Every prop's `ref` and
|
|
1673
|
+
* `ui.valueHelp` binding is stripped when the caller cannot read the target
|
|
1674
|
+
* table, or cannot see a field the pointer names there (the same rule as the
|
|
1675
|
+
* `/meta` type); an unresolvable binding is stripped too. Never mutates
|
|
1676
|
+
* `serialized` (the base controller caches it); returns it itself when
|
|
1677
|
+
* nothing is stripped.
|
|
1678
|
+
*
|
|
1679
|
+
* @param serialized - the form's serialized schema as moost-db served it
|
|
1680
|
+
* @param formType - the runtime form type the schema was serialized from
|
|
1681
|
+
*/
|
|
1682
|
+
async function applyArbacFormOverlay(serialized, formType) {
|
|
1683
|
+
if (!formType) return serialized;
|
|
1684
|
+
const types = /* @__PURE__ */ new Set();
|
|
1685
|
+
collectFormTargets(formType, serialized, types, 0);
|
|
1686
|
+
if (types.size === 0 && !hasUnresolvedBinding(formType, serialized, 0)) return serialized;
|
|
1687
|
+
const readable = /* @__PURE__ */ new Map();
|
|
1688
|
+
await Promise.all([...types].map(async (type) => {
|
|
1689
|
+
const read = await evaluateTypeRead(type);
|
|
1690
|
+
readable.set(type, read ? buildScopeVisibility(read.scopes, read.table) : null);
|
|
1691
|
+
}));
|
|
1692
|
+
return stripFormTargets(formType, serialized, readable, 0);
|
|
1693
|
+
}
|
|
1694
|
+
const formTargetsOf = (entry, node) => {
|
|
1695
|
+
const out = [];
|
|
1696
|
+
if (node.ref) {
|
|
1697
|
+
const ref = refTargetOf(entry);
|
|
1698
|
+
out.push(ref ?? null);
|
|
1699
|
+
}
|
|
1700
|
+
if (node.metadata?.[UI_VALUE_HELP] !== void 0) {
|
|
1701
|
+
const binding = bindingOf(entry);
|
|
1702
|
+
if (binding !== void 0) out.push(binding);
|
|
1703
|
+
}
|
|
1704
|
+
if (node.ref && node.ref.type?.metadata?.[UI_VALUE_HELP] !== void 0) out.push(refTypeBindingOf(entry) ?? null);
|
|
1705
|
+
return out;
|
|
1706
|
+
};
|
|
1707
|
+
/** Pairs the runtime and serialized trees over EVERY child container (props, of, items). */
|
|
1708
|
+
function formChildren(runtime, node) {
|
|
1709
|
+
const rt = runtime.type;
|
|
1710
|
+
const def = node.type;
|
|
1711
|
+
const out = [];
|
|
1712
|
+
if (rt.props && def.props) {
|
|
1713
|
+
for (const [name, child] of rt.props) if (def.props[name]) out.push([
|
|
1714
|
+
child,
|
|
1715
|
+
def.props[name],
|
|
1716
|
+
{ prop: name }
|
|
1717
|
+
]);
|
|
1718
|
+
}
|
|
1719
|
+
if (rt.of && !Array.isArray(rt.of) && def.of && !Array.isArray(def.of)) out.push([
|
|
1720
|
+
rt.of,
|
|
1721
|
+
def.of,
|
|
1722
|
+
{ of: true }
|
|
1723
|
+
]);
|
|
1724
|
+
if (Array.isArray(rt.items) && Array.isArray(def.items)) rt.items.forEach((child, i) => def.items[i] && out.push([
|
|
1725
|
+
child,
|
|
1726
|
+
def.items[i],
|
|
1727
|
+
{ item: i }
|
|
1728
|
+
]));
|
|
1729
|
+
def.propsPatterns?.forEach((p, i) => {
|
|
1730
|
+
const match = rt.propsPatterns?.find((r) => r.pattern?.source === p.pattern?.source && r.pattern?.flags === p.pattern?.flags);
|
|
1731
|
+
if (match) out.push([
|
|
1732
|
+
match.def,
|
|
1733
|
+
p.def,
|
|
1734
|
+
{ pattern: i }
|
|
1735
|
+
]);
|
|
1736
|
+
});
|
|
1737
|
+
return out;
|
|
1738
|
+
}
|
|
1739
|
+
/** Nested-relation walk bound — serialized nav bodies are finite, this only guards pathological input. */
|
|
1740
|
+
const MAX_NAV_DEPTH = 8;
|
|
1741
|
+
/** The form walk bound; past it every `ref` / binding is stripped (fail closed). */
|
|
1742
|
+
const MAX_FORM_DEPTH = MAX_NAV_DEPTH * 2;
|
|
1743
|
+
function serializedChildren(node) {
|
|
1744
|
+
const def = node.type;
|
|
1745
|
+
return [
|
|
1746
|
+
...Object.values(def.props ?? {}),
|
|
1747
|
+
...def.of ? [def.of] : [],
|
|
1748
|
+
...def.items ?? [],
|
|
1749
|
+
...(def.propsPatterns ?? []).map((p) => p.def)
|
|
1750
|
+
];
|
|
1751
|
+
}
|
|
1752
|
+
function serializedHasTarget(node) {
|
|
1753
|
+
return !!node.ref || node.metadata?.[UI_VALUE_HELP] !== void 0 || serializedChildren(node).some(serializedHasTarget);
|
|
1754
|
+
}
|
|
1755
|
+
function stripAllTargets(node) {
|
|
1756
|
+
if (!serializedHasTarget(node)) return node;
|
|
1757
|
+
let out = node;
|
|
1758
|
+
if (out.ref) out = withoutRef(out);
|
|
1759
|
+
if (out.metadata?.[UI_VALUE_HELP] !== void 0) out = withoutBinding(out);
|
|
1760
|
+
const def = out.type;
|
|
1761
|
+
const next = { ...def };
|
|
1762
|
+
if (def.props) next.props = Object.fromEntries(Object.entries(def.props).map(([k, v]) => [k, stripAllTargets(v)]));
|
|
1763
|
+
if (def.of) next.of = stripAllTargets(def.of);
|
|
1764
|
+
if (def.items) next.items = def.items.map((i) => stripAllTargets(i));
|
|
1765
|
+
if (def.propsPatterns) next.propsPatterns = def.propsPatterns.map((p) => ({
|
|
1766
|
+
...p,
|
|
1767
|
+
def: stripAllTargets(p.def)
|
|
1768
|
+
}));
|
|
1769
|
+
return {
|
|
1770
|
+
...out,
|
|
1771
|
+
type: next
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
function collectFormTargets(runtime, node, into, depth) {
|
|
1775
|
+
if (depth >= MAX_FORM_DEPTH) return;
|
|
1776
|
+
for (const t of formTargetsOf(runtime, node)) if (t) into.add(t.type);
|
|
1777
|
+
for (const [child, childNode] of formChildren(runtime, node)) collectFormTargets(child, childNode, into, depth + 1);
|
|
1778
|
+
}
|
|
1779
|
+
function hasUnresolvedBinding(runtime, node, depth) {
|
|
1780
|
+
if (depth >= MAX_FORM_DEPTH) return serializedHasTarget(node);
|
|
1781
|
+
if (formTargetsOf(runtime, node).includes(null)) return true;
|
|
1782
|
+
return formChildren(runtime, node).some(([c, n]) => hasUnresolvedBinding(c, n, depth + 1));
|
|
1783
|
+
}
|
|
1784
|
+
function stripFormTargets(runtime, node, readable, depth) {
|
|
1785
|
+
if (depth >= MAX_FORM_DEPTH) return stripAllTargets(node);
|
|
1786
|
+
const visible = (t) => {
|
|
1787
|
+
if (!t) return false;
|
|
1788
|
+
const targetVis = readable.get(t.type);
|
|
1789
|
+
return !!targetVis && t.fields.every((f) => isMetaFieldVisible(f, targetVis));
|
|
1790
|
+
};
|
|
1791
|
+
const out = gateOwn(node, runtime, (t) => visible(t));
|
|
1792
|
+
const children = formChildren(runtime, node);
|
|
1793
|
+
let changed;
|
|
1794
|
+
for (const [child, childNode, key] of children) {
|
|
1795
|
+
const next = stripFormTargets(child, childNode, readable, depth + 1);
|
|
1796
|
+
if (next === childNode) continue;
|
|
1797
|
+
const base = changed ?? out.type;
|
|
1798
|
+
if ("prop" in key) changed = {
|
|
1799
|
+
...base,
|
|
1800
|
+
props: {
|
|
1801
|
+
...base.props,
|
|
1802
|
+
[key.prop]: next
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
else if ("pattern" in key) changed = {
|
|
1806
|
+
...base,
|
|
1807
|
+
propsPatterns: base.propsPatterns.map((o, i) => i === key.pattern ? {
|
|
1808
|
+
...o,
|
|
1809
|
+
def: next
|
|
1810
|
+
} : o)
|
|
1811
|
+
};
|
|
1812
|
+
else if ("item" in key) changed = {
|
|
1813
|
+
...base,
|
|
1814
|
+
items: base.items.map((o, i) => i === key.item ? next : o)
|
|
1815
|
+
};
|
|
1816
|
+
else changed = {
|
|
1817
|
+
...base,
|
|
1818
|
+
of: next
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
return changed ? {
|
|
1822
|
+
...out,
|
|
1823
|
+
type: changed
|
|
1824
|
+
} : out;
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* `fields[*].groupable` follows the `controls.$groupBy` policy, mirroring the
|
|
1828
|
+
* 403 `applyArbacControls` raises: `false` drops the flag everywhere, a
|
|
1829
|
+
* whitelist keeps it only on the listed paths. Returns `meta` itself when
|
|
1830
|
+
* nothing is stripped.
|
|
1831
|
+
*
|
|
1832
|
+
* @since 0.1.75
|
|
1833
|
+
*/
|
|
1834
|
+
function gateGroupable(meta, gate) {
|
|
1835
|
+
if (gate === void 0 || gate === true) return meta;
|
|
1836
|
+
const stripped = (path, field) => field.groupable && (gate === false || !gate.includes(path));
|
|
1837
|
+
const entries = Object.entries(meta.fields);
|
|
1838
|
+
if (!entries.some(([path, field]) => stripped(path, field))) return meta;
|
|
1839
|
+
const fields = {};
|
|
1840
|
+
for (const [path, field] of entries) if (stripped(path, field)) {
|
|
1841
|
+
const { groupable: _groupable, ...rest } = field;
|
|
1842
|
+
fields[path] = rest;
|
|
1843
|
+
} else fields[path] = field;
|
|
1844
|
+
return {
|
|
1845
|
+
...meta,
|
|
1846
|
+
fields
|
|
1847
|
+
};
|
|
1333
1848
|
}
|
|
1334
1849
|
const metaNamesCache = /* @__PURE__ */ new WeakMap();
|
|
1335
1850
|
/** The relation names `/meta`'s serialized type reaches: root relations, then nav props of each nav body. */
|
|
@@ -1338,7 +1853,7 @@ function metaRelationNames(meta) {
|
|
|
1338
1853
|
}
|
|
1339
1854
|
function buildMetaRelationNames(meta) {
|
|
1340
1855
|
const tree = /* @__PURE__ */ new Map();
|
|
1341
|
-
const rootProps = objectProps(meta.type);
|
|
1856
|
+
const rootProps = objectProps(inlineTypeRefs(meta.type));
|
|
1342
1857
|
for (const r of meta.relations) {
|
|
1343
1858
|
const nested = /* @__PURE__ */ new Map();
|
|
1344
1859
|
tree.set(r.name, nested);
|
|
@@ -1347,8 +1862,6 @@ function buildMetaRelationNames(meta) {
|
|
|
1347
1862
|
}
|
|
1348
1863
|
return tree;
|
|
1349
1864
|
}
|
|
1350
|
-
/** Nested-relation walk bound — serialized nav bodies are finite, this only guards pathological input. */
|
|
1351
|
-
const MAX_NAV_DEPTH = 8;
|
|
1352
1865
|
function collectNavNames(node, into, depth) {
|
|
1353
1866
|
if (depth >= MAX_NAV_DEPTH) return;
|
|
1354
1867
|
const props = objectProps(node);
|
|
@@ -1382,7 +1895,7 @@ async function resolveRefTargets(root, meta) {
|
|
|
1382
1895
|
if (sub) visit(sub, prop, depth + 1);
|
|
1383
1896
|
}
|
|
1384
1897
|
};
|
|
1385
|
-
visit(root, meta.type, 0);
|
|
1898
|
+
visit(root, inlineTypeRefs(meta.type), 0);
|
|
1386
1899
|
const out = /* @__PURE__ */ new Map();
|
|
1387
1900
|
await Promise.all([...types].map(async (type) => {
|
|
1388
1901
|
const read = await evaluateTypeRead(type);
|
|
@@ -1399,8 +1912,7 @@ function foreignRefTypes(table) {
|
|
|
1399
1912
|
const out = /* @__PURE__ */ new Set();
|
|
1400
1913
|
for (const [path, entry] of flatMap) {
|
|
1401
1914
|
if (hasSelfOrAncestor(nav, path)) continue;
|
|
1402
|
-
const target
|
|
1403
|
-
if (target && target.type !== table.type) out.add(target.type);
|
|
1915
|
+
for (const target of targetsOf(entry)) if (target.type !== table.type) out.add(target.type);
|
|
1404
1916
|
}
|
|
1405
1917
|
return out;
|
|
1406
1918
|
});
|
|
@@ -1624,6 +2136,17 @@ const READ_ENDPOINTS = /* @__PURE__ */ new Set([
|
|
|
1624
2136
|
"one"
|
|
1625
2137
|
]);
|
|
1626
2138
|
const requestScopesKey = key("arbac.db.scopes");
|
|
2139
|
+
/**
|
|
2140
|
+
* `true` while the current request is an insert in ignore mode
|
|
2141
|
+
* (`?$onConflict=ignore`): conflicting rows are skipped, so the post-write
|
|
2142
|
+
* WITH CHECK never sees them and the write guard must refuse what it cannot
|
|
2143
|
+
* check up front.
|
|
2144
|
+
*
|
|
2145
|
+
* @since 0.1.75
|
|
2146
|
+
*/
|
|
2147
|
+
function isInsertIgnoreRequest() {
|
|
2148
|
+
return resolved()?.insertIgnore === true;
|
|
2149
|
+
}
|
|
1627
2150
|
/** Store the event's resolved scopes (for the handler `ids` resolves to). */
|
|
1628
2151
|
function storeResolved(ctx, ids, source, scopes) {
|
|
1629
2152
|
ctx.set(requestScopesKey, {
|
|
@@ -1752,6 +2275,7 @@ async function prepareArbacRequest(ctx, readable) {
|
|
|
1752
2275
|
return;
|
|
1753
2276
|
}
|
|
1754
2277
|
const scopes = await resolveRequestScopes();
|
|
2278
|
+
if (ctx.endpoint === "insert" && ctx.onConflict === "ignore") resolved().insertIgnore = true;
|
|
1755
2279
|
if (READ_ENDPOINTS.has(ctx.endpoint)) enforceRelationGates(await resolveRequestRelations(scopes, ctx.controls, readable, ctx.filter), visibilityFor(scopes, readable));
|
|
1756
2280
|
}
|
|
1757
2281
|
/**
|
|
@@ -2036,6 +2560,28 @@ function checkFields(filter) {
|
|
|
2036
2560
|
if (containsRelationFilter(filter)) throw new UnsupportedCheckError("relational predicate");
|
|
2037
2561
|
return new Set(computeInsights(filter).keys());
|
|
2038
2562
|
}
|
|
2563
|
+
/**
|
|
2564
|
+
* The in-memory WITH CHECK verdict for rows about to be inserted: `false` when
|
|
2565
|
+
* a row the evaluator can decide fails the check, `true` when every row passes,
|
|
2566
|
+
* `undefined` when some row cannot be decided up front — the check is not
|
|
2567
|
+
* modelled (relational, `$regex`, …) or reads a field the row omits (a database
|
|
2568
|
+
* default or generated value).
|
|
2569
|
+
*/
|
|
2570
|
+
function insertRowsVerdict(rows, check) {
|
|
2571
|
+
let predicate;
|
|
2572
|
+
let fields;
|
|
2573
|
+
try {
|
|
2574
|
+
predicate = compileScopeCheck(check);
|
|
2575
|
+
fields = checkFields(check);
|
|
2576
|
+
} catch (error) {
|
|
2577
|
+
if (error instanceof UnsupportedCheckError) return void 0;
|
|
2578
|
+
throw error;
|
|
2579
|
+
}
|
|
2580
|
+
let verdict = true;
|
|
2581
|
+
for (const row of rows) if ([...fields].some((f) => getPath(row, f) === void 0)) verdict = void 0;
|
|
2582
|
+
else if (!predicate(row)) return false;
|
|
2583
|
+
return verdict;
|
|
2584
|
+
}
|
|
2039
2585
|
function same(a, b) {
|
|
2040
2586
|
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
2041
2587
|
return a === b;
|
|
@@ -2184,9 +2730,14 @@ async function assertWrittenRowsInCheck(ctx, check) {
|
|
|
2184
2730
|
* - `checkRefs` — every enforced FK the write sets must reference rows the
|
|
2185
2731
|
* caller can read on the target table (→ 403);
|
|
2186
2732
|
* - WITH CHECK when the adapter cannot roll back — validated here, before
|
|
2187
|
-
* the write (see {@link checkArbacWrite} for the transactional path)
|
|
2188
|
-
|
|
2189
|
-
|
|
2733
|
+
* the write (see {@link checkArbacWrite} for the transactional path);
|
|
2734
|
+
* - WITH CHECK of an insert, even on a transactional adapter, evaluated on
|
|
2735
|
+
* the submitted rows before the write when the in-memory evaluator can
|
|
2736
|
+
* model it (a forbidden row → 403, never a duplicate-key 409; since
|
|
2737
|
+
* 0.1.75) — and, with `ignoreConflicts` (`?$onConflict=ignore`), refused
|
|
2738
|
+
* whenever it cannot, since the post-write check never sees skipped rows.
|
|
2739
|
+
*/
|
|
2740
|
+
async function guardArbacWrite(ctx, scopes, table, refs, ignoreConflicts = false) {
|
|
2190
2741
|
const eff = effectiveScope(scopes);
|
|
2191
2742
|
if (eff.filter && hasPreImage(ctx.action)) await assertIdFiltersInScope(table, ctx.rows.map((_row, i) => {
|
|
2192
2743
|
const filter = ctx.filterFor(i);
|
|
@@ -2194,7 +2745,12 @@ async function guardArbacWrite(ctx, scopes, table, refs) {
|
|
|
2194
2745
|
return filter;
|
|
2195
2746
|
}), eff.filter);
|
|
2196
2747
|
await assertRefsInScope(ctx, scopes, refs);
|
|
2197
|
-
if (eff.check
|
|
2748
|
+
if (!eff.check) return;
|
|
2749
|
+
if (!table.getAdapter().isInTransaction()) await assertCheckBeforeWrite(ctx, eff.check);
|
|
2750
|
+
else if (!hasPreImage(ctx.action)) {
|
|
2751
|
+
const verdict = insertRowsVerdict(ctx.rows, eff.check);
|
|
2752
|
+
if (verdict === false || verdict === void 0 && ignoreConflicts) throw new HttpError(403, OUT_OF_SCOPE);
|
|
2753
|
+
}
|
|
2198
2754
|
}
|
|
2199
2755
|
/**
|
|
2200
2756
|
* WITH CHECK after the write, inside its transaction: every written row must
|
|
@@ -2301,6 +2857,14 @@ let AsArbacDbController = class AsArbacDbController extends AsDbController {
|
|
|
2301
2857
|
return applyArbacMetaOverlay(meta, this.readable);
|
|
2302
2858
|
}
|
|
2303
2859
|
/**
|
|
2860
|
+
* `GET /meta/form/:name` under the `/meta` overlay's pointer rule: a form
|
|
2861
|
+
* prop's `ref` / `ui.valueHelp` binding to a table the caller cannot read
|
|
2862
|
+
* (or whose named fields they cannot see) is stripped.
|
|
2863
|
+
*/
|
|
2864
|
+
async metaForm(name) {
|
|
2865
|
+
return applyArbacFormOverlay(await super.metaForm(name), getControllerFormType(this.constructor, name));
|
|
2866
|
+
}
|
|
2867
|
+
/**
|
|
2304
2868
|
* `GET /meta/form/:name` serves a form only when the caller may run at
|
|
2305
2869
|
* least one of the actions taking it as input (else the unknown-form 404).
|
|
2306
2870
|
*/
|
|
@@ -2374,7 +2938,7 @@ let AsArbacDbController = class AsArbacDbController extends AsDbController {
|
|
|
2374
2938
|
}
|
|
2375
2939
|
/** In-transaction stage — USING, `checkRefs`, and WITH CHECK on non-transactional adapters. */
|
|
2376
2940
|
guardWrite(ctx) {
|
|
2377
|
-
return guardArbacWrite(ctx, requireRequestScopes(), this.guardedTable, this.readable);
|
|
2941
|
+
return guardArbacWrite(ctx, requireRequestScopes(), this.guardedTable, this.readable, isInsertIgnoreRequest());
|
|
2378
2942
|
}
|
|
2379
2943
|
/** USING for deletes: the exact row the delete targets must match the scope filter (else 404). */
|
|
2380
2944
|
guardRemove(ctx) {
|
|
@@ -2527,6 +3091,14 @@ let AsArbacDbReadableController = class AsArbacDbReadableController extends AsDb
|
|
|
2527
3091
|
applyMetaOverlay(meta) {
|
|
2528
3092
|
return applyArbacMetaOverlay(meta, this.readable);
|
|
2529
3093
|
}
|
|
3094
|
+
/**
|
|
3095
|
+
* `GET /meta/form/:name` under the `/meta` overlay's pointer rule: a form
|
|
3096
|
+
* prop's `ref` / `ui.valueHelp` binding to a table the caller cannot read
|
|
3097
|
+
* (or whose named fields they cannot see) is stripped.
|
|
3098
|
+
*/
|
|
3099
|
+
async metaForm(name) {
|
|
3100
|
+
return applyArbacFormOverlay(await super.metaForm(name), getControllerFormType(this.constructor, name));
|
|
3101
|
+
}
|
|
2530
3102
|
/** Same contract as {@link AsArbacDbController.authorizeForm}. */
|
|
2531
3103
|
authorizeForm(_name, actionNames) {
|
|
2532
3104
|
return authorizeArbacForm(actionNames);
|
|
@@ -2551,7 +3123,7 @@ let AsArbacDbReadableController = class AsArbacDbReadableController extends AsDb
|
|
|
2551
3123
|
AsArbacDbReadableController = __decorate([Inherit()], AsArbacDbReadableController);
|
|
2552
3124
|
//#endregion
|
|
2553
3125
|
//#region src/db/as-arbac-value-help-controller.ts
|
|
2554
|
-
function valueHelpHost(
|
|
3126
|
+
function valueHelpHost(boundType, primaryKey, searchableFields) {
|
|
2555
3127
|
const ids = primaryKey ? [primaryKey] : [];
|
|
2556
3128
|
return {
|
|
2557
3129
|
primaryKey,
|
|
@@ -2559,7 +3131,8 @@ function valueHelpHost(fields, primaryKey, searchableFields) {
|
|
|
2559
3131
|
source: {
|
|
2560
3132
|
primaryKeys: ids,
|
|
2561
3133
|
preferredId: ids,
|
|
2562
|
-
|
|
3134
|
+
type: boundType,
|
|
3135
|
+
flatMap: boundType.type.props ?? /* @__PURE__ */ new Map()
|
|
2563
3136
|
}
|
|
2564
3137
|
};
|
|
2565
3138
|
}
|
|
@@ -2606,8 +3179,10 @@ async function applyValueHelpMetaOverlay(host, meta) {
|
|
|
2606
3179
|
}
|
|
2607
3180
|
let AsArbacValueHelpController = class AsArbacValueHelpController extends AsValueHelpController {
|
|
2608
3181
|
_arbacHost;
|
|
3182
|
+
/** Makes this controller the binding / `ref` policy source for its bound type's ARBAC resource. */
|
|
3183
|
+
_arbacValueHelpTarget = registerArbacValueHelpTarget(this, this.arbacHost.source);
|
|
2609
3184
|
get arbacHost() {
|
|
2610
|
-
return this._arbacHost ??= valueHelpHost(this.
|
|
3185
|
+
return this._arbacHost ??= valueHelpHost(this.boundType, this.primaryKey, this.searchableFields);
|
|
2611
3186
|
}
|
|
2612
3187
|
async prepareRequest(_ctx) {
|
|
2613
3188
|
await resolveRequestScopes();
|
|
@@ -2670,8 +3245,10 @@ __decorate([
|
|
|
2670
3245
|
AsArbacValueHelpController = __decorate([Inherit()], AsArbacValueHelpController);
|
|
2671
3246
|
let AsArbacJsonValueHelpController = class AsArbacJsonValueHelpController extends AsJsonValueHelpController {
|
|
2672
3247
|
_arbacHost;
|
|
3248
|
+
/** Makes this controller the binding / `ref` policy source for its bound type's ARBAC resource. */
|
|
3249
|
+
_arbacValueHelpTarget = registerArbacValueHelpTarget(this, this.arbacHost.source);
|
|
2673
3250
|
get arbacHost() {
|
|
2674
|
-
return this._arbacHost ??= valueHelpHost(this.
|
|
3251
|
+
return this._arbacHost ??= valueHelpHost(this.boundType, this.primaryKey, this.searchableFields);
|
|
2675
3252
|
}
|
|
2676
3253
|
async prepareRequest(_ctx) {
|
|
2677
3254
|
await resolveRequestScopes();
|
|
@@ -2779,12 +3356,12 @@ async function useArbacDbScope() {
|
|
|
2779
3356
|
currentAll: async () => rows.map(() => null),
|
|
2780
3357
|
filterFor: () => null
|
|
2781
3358
|
}, scopes, table),
|
|
2782
|
-
writeOptions: (table) => ({
|
|
3359
|
+
writeOptions: (table, opts) => ({
|
|
2783
3360
|
isFieldVisible: visibleOn(table),
|
|
2784
3361
|
guard: async (ctx) => {
|
|
2785
3362
|
const rows = ctx;
|
|
2786
3363
|
assertNestedWritesAllowed(rows.rows, scopes, navFieldsOf(table));
|
|
2787
|
-
await guardArbacWrite(rows, scopes, table, table);
|
|
3364
|
+
await guardArbacWrite(rows, scopes, table, table, opts?.onConflict === "ignore");
|
|
2788
3365
|
},
|
|
2789
3366
|
check: (ctx) => checkArbacWrite(ctx, scopes)
|
|
2790
3367
|
}),
|
|
@@ -2,7 +2,7 @@ import { ControlGate, TProjection, TProjectionChildren, TScopeFieldRules, TScope
|
|
|
2
2
|
import { Mate, TClassConstructor, TMateParamMeta, TMoostMetadata } from "moost";
|
|
3
3
|
import { FilterExpr, TDbRemoveGuardContext, TDbWriteAction, TDbWriteCheckContext, TDbWriteGuardContext, TMetaResponse, UniqueryControls } from "@atscript/db";
|
|
4
4
|
import { AsDbController, TDbActionScopeContext, TDbControlsType, TDbRequestContext } from "@atscript/moost-db";
|
|
5
|
-
import { NavPropsOf, OwnPropsOf, TAtscriptAnnotatedType, TAtscriptDataType } from "@atscript/typescript/utils";
|
|
5
|
+
import { NavPropsOf, OwnPropsOf, TAtscriptAnnotatedType, TAtscriptDataType, TSerializedAnnotatedType } from "@atscript/typescript/utils";
|
|
6
6
|
//#region src/arbac.mate.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* ARBAC metadata fields attached to classes and methods by ARBAC decorators.
|
|
@@ -314,6 +314,12 @@ declare class AsArbacDbController<T extends TAtscriptAnnotatedType = TAtscriptAn
|
|
|
314
314
|
*/
|
|
315
315
|
protected validateControls(controls: Record<string, unknown>, type: TDbControlsType): string | undefined;
|
|
316
316
|
protected applyMetaOverlay(meta: TMetaResponse): Promise<TMetaResponse>;
|
|
317
|
+
/**
|
|
318
|
+
* `GET /meta/form/:name` under the `/meta` overlay's pointer rule: a form
|
|
319
|
+
* prop's `ref` / `ui.valueHelp` binding to a table the caller cannot read
|
|
320
|
+
* (or whose named fields they cannot see) is stripped.
|
|
321
|
+
*/
|
|
322
|
+
metaForm(name: string): Promise<TSerializedAnnotatedType>;
|
|
317
323
|
/**
|
|
318
324
|
* `GET /meta/form/:name` serves a form only when the caller may run at
|
|
319
325
|
* least one of the actions taking it as input (else the unknown-form 404).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aooth/arbac-moost",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.76",
|
|
4
4
|
"description": "Moost RBAC integration for aoothjs (migrated from @moostjs/arbac)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abac",
|
|
@@ -60,31 +60,31 @@
|
|
|
60
60
|
"access": "public"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@uniqu/core": "^0.1.
|
|
64
|
-
"@aooth/
|
|
65
|
-
"@aooth/
|
|
66
|
-
"@aooth/arbac": "0.1.
|
|
63
|
+
"@uniqu/core": "^0.1.13",
|
|
64
|
+
"@aooth/user": "0.1.76",
|
|
65
|
+
"@aooth/arbac": "0.1.76",
|
|
66
|
+
"@aooth/arbac-core": "0.1.76"
|
|
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.100",
|
|
70
|
+
"@atscript/db": "^0.1.149",
|
|
71
|
+
"@atscript/db-sql-tools": "^0.1.149",
|
|
72
|
+
"@atscript/db-sqlite": "^0.1.149",
|
|
73
|
+
"@atscript/moost-db": "^0.1.149",
|
|
74
|
+
"@atscript/typescript": "^0.1.100",
|
|
75
|
+
"@moostjs/event-http": "^0.6.45",
|
|
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.100"
|
|
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.
|
|
81
|
+
"@atscript/db": "^0.1.149",
|
|
82
|
+
"@atscript/moost-db": "^0.1.149",
|
|
83
|
+
"@atscript/typescript": "^0.1.100",
|
|
84
|
+
"@moostjs/event-http": "^0.6.45",
|
|
85
85
|
"@wooksjs/event-core": "^0.7.26",
|
|
86
86
|
"@wooksjs/event-http": "^0.7.26",
|
|
87
|
-
"moost": "^0.6.
|
|
87
|
+
"moost": "^0.6.45"
|
|
88
88
|
},
|
|
89
89
|
"peerDependenciesMeta": {
|
|
90
90
|
"@atscript/db": {
|