@aooth/arbac-moost 0.1.74 → 0.1.75

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-CKjmUhkx.mjs";
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-CKjmUhkx.mjs";
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): TWriteOptions<Row>;
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.readable
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].readable
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
- const pruned = pruneSerializedType(prop, child);
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
- return {
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, level))
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 terminal;
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: def.ref.type(),
1214
- field: def.ref.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
- * A serialized `ref` at `path` survives when its target field is visible:
1222
- * a self-reference (e.g. a derived column's source) against this level's
1223
- * visibility; a foreign one against the caller's visibility on the target
1224
- * table (`refTargets`, stripped when the caller cannot read it). Without
1225
- * runtime schema info (`vis.table.flatMap`) or `refTargets`, refs are kept.
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 isRefVisible(path, vis, refTargets) {
1228
- const table = vis.table;
1229
- if (!table?.flatMap || !refTargets) return true;
1230
- const target = refTargetOf(table.flatMap.get(path));
1231
- if (!target || !target.field) return true;
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 = refTargetOf(entry);
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
- async function guardArbacWrite(ctx, scopes, table, refs) {
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 && !table.getAdapter().isInTransaction()) await assertCheckBeforeWrite(ctx, 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(fields, primaryKey, searchableFields) {
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
- flatMap: fields
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.fieldMeta, this.primaryKey, this.searchableFields);
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.fieldMeta, this.primaryKey, this.searchableFields);
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.74",
3
+ "version": "0.1.75",
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.12",
64
- "@aooth/arbac-core": "0.1.74",
65
- "@aooth/user": "0.1.74",
66
- "@aooth/arbac": "0.1.74"
63
+ "@uniqu/core": "^0.1.13",
64
+ "@aooth/arbac": "0.1.75",
65
+ "@aooth/user": "0.1.75",
66
+ "@aooth/arbac-core": "0.1.75"
67
67
  },
68
68
  "devDependencies": {
69
- "@atscript/core": "^0.1.99",
70
- "@atscript/db": "^0.1.147",
71
- "@atscript/db-sql-tools": "^0.1.147",
72
- "@atscript/db-sqlite": "^0.1.147",
73
- "@atscript/moost-db": "^0.1.147",
74
- "@atscript/typescript": "^0.1.99",
75
- "@moostjs/event-http": "^0.6.43",
69
+ "@atscript/core": "^0.1.100",
70
+ "@atscript/db": "^0.1.148",
71
+ "@atscript/db-sql-tools": "^0.1.148",
72
+ "@atscript/db-sqlite": "^0.1.148",
73
+ "@atscript/moost-db": "^0.1.148",
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.99"
78
+ "unplugin-atscript": "^0.1.100"
79
79
  },
80
80
  "peerDependencies": {
81
- "@atscript/db": "^0.1.147",
82
- "@atscript/moost-db": "^0.1.147",
83
- "@atscript/typescript": "^0.1.99",
84
- "@moostjs/event-http": "^0.6.43",
81
+ "@atscript/db": "^0.1.148",
82
+ "@atscript/moost-db": "^0.1.148",
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.43"
87
+ "moost": "^0.6.45"
88
88
  },
89
89
  "peerDependenciesMeta": {
90
90
  "@atscript/db": {