|
|
|
@@ -117,6 +117,43 @@ function answerUnnamedMember(target: object, prop: string | symbol, where: strin
|
|
|
|
|
throw refusalOf(prop, where);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** `"0"`, `"12"` - an array index, in the canonical form a property key takes. */
|
|
|
|
|
const INDEX = /^(0|[1-9][0-9]*)$/;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The descriptor of a member, filtered the same way reading it is.
|
|
|
|
|
*
|
|
|
|
|
* `Object.getOwnPropertyDescriptor` and everything built on it - `Object.keys`,
|
|
|
|
|
* `Object.entries`, a spread, `JSON.stringify`, a console inspection - read a property
|
|
|
|
|
* WITHOUT going through the `get` trap. A view that filtered `get` alone therefore handed
|
|
|
|
|
* the raw value straight back to whoever asked for the descriptor instead: measured
|
|
|
|
|
* 2026-09-24 on the first fix of this file, where `getOwnPropertyDescriptor(result, "data")`
|
|
|
|
|
* returned the unfiltered collection and `...(result, "subscription")` the ORM handle that
|
|
|
|
|
* `get` refuses. A second door into the same room, opened by a trap nobody wrote.
|
|
|
|
|
*
|
|
|
|
|
* So the descriptor is answered by REREADING through the view: `read` is the very function
|
|
|
|
|
* the `get` trap calls, passed in, so the two cannot drift apart. A member `get` refuses
|
|
|
|
|
* throws here too, at the same place and with the same message.
|
|
|
|
|
*/
|
|
|
|
|
function filteredDescriptor(
|
|
|
|
|
target: object,
|
|
|
|
|
prop: string | symbol,
|
|
|
|
|
read: () => unknown,
|
|
|
|
|
): PropertyDescriptor | undefined {
|
|
|
|
|
const d = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
|
|
|
if (d === undefined) return undefined;
|
|
|
|
|
// An accessor carries no value of its own; the `get` trap is what answers it.
|
|
|
|
|
if (!("value" in d)) return d;
|
|
|
|
|
const value = read();
|
|
|
|
|
if (Object.is(d.value, value)) return d;
|
|
|
|
|
// A property that is neither configurable nor writable cannot be REPORTED with another
|
|
|
|
|
// value without breaking the invariants a proxy is held to, and reporting the target's
|
|
|
|
|
// would be the leak. Nothing on either shape this file wraps is one - a result object's
|
|
|
|
|
// members and an array's indices are both - so this is a guard, not a path.
|
|
|
|
|
if (d.configurable !== true && d.writable !== true) throw refusalOf(prop, "its descriptor");
|
|
|
|
|
return { ...d, value };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A read-filtered VIEW over a reactive set (a `DeepSignalSet`, or any Set-like).
|
|
|
|
|
*
|
|
|
|
@@ -139,6 +176,14 @@ function answerUnnamedMember(target: object, prop: string | symbol, where: strin
|
|
|
|
|
* hits it exactly what to do: add the member here, filtered. A boundary whose unknown
|
|
|
|
|
* cases leak is not a boundary.
|
|
|
|
|
*
|
|
|
|
|
* ── And `get` is not the only door ────────────────────────────────────────
|
|
|
|
|
* Three traps answer the same question and all three have to give the same answer:
|
|
|
|
|
* `get`, `getOwnPropertyDescriptor` (which `Object.keys`, a spread, `JSON.stringify` and
|
|
|
|
|
* any console inspection go through, WITHOUT touching `get`) and, on the array form,
|
|
|
|
|
* `has` and `ownKeys`. Each of them was forwarding to the target while `get` filtered.
|
|
|
|
|
* They are wired to one `read` function below rather than to three copies of the rule,
|
|
|
|
|
* because a boundary made of parallel switch statements is a boundary that drifts.
|
|
|
|
|
*
|
|
|
|
|
* ── Why the default is decided by TYPE, and not by a list of names ────────
|
|
|
|
|
* Until 2026-09-24 that reasoning was applied to FUNCTION members alone: everything else
|
|
|
|
|
* was forwarded raw, on the recorded ground that "a plain property carries no items". That
|
|
|
|
@@ -163,87 +208,130 @@ export function makeReadFilteredView<S extends object>(set: S, caps: CapRegistry
|
|
|
|
|
for (const item of target as Iterable<unknown>) if (keep(item)) out.push(item);
|
|
|
|
|
return out;
|
|
|
|
|
};
|
|
|
|
|
/**
|
|
|
|
|
* Is the collection ARRAY-shaped? An `orderBy` config makes upstream's `signalObject` a
|
|
|
|
|
* `DeepSignal<ReadOnlyArray>` instead of a `DeepSignalSet`, and an array is addressed by
|
|
|
|
|
* INDEX and counts itself with `length`. Decided from the target once, so the view never
|
|
|
|
|
* grows a member the collection it wraps does not have.
|
|
|
|
|
*/
|
|
|
|
|
const arrayLike = typeof (set as { length?: unknown }).length === "number";
|
|
|
|
|
/** The index a key names, or `null` when it names no position in this collection. */
|
|
|
|
|
const indexOf = (prop: string | symbol): number | null =>
|
|
|
|
|
arrayLike && typeof prop === "string" && INDEX.test(prop) ? Number(prop) : null;
|
|
|
|
|
|
|
|
|
|
/** What the view answers for one member. The `get` trap and the descriptor trap BOTH
|
|
|
|
|
* call this, so neither can answer what the other refuses. */
|
|
|
|
|
const read = (target: object, prop: string | symbol, receiver: unknown): unknown => {
|
|
|
|
|
if (prop === Symbol.iterator) return function* () { yield* kept(target); };
|
|
|
|
|
// HOW MANY, under both spellings. `data` is a `DeepSignalSet` for an unordered read
|
|
|
|
|
// and an array-like `DeepSignal<ReadOnlyArray>` once the config asks for an
|
|
|
|
|
// `orderBy`, and `length` is a number - so it used to pass as a primitive and
|
|
|
|
|
// publish the UNFILTERED count, which is showing more than the holder may read.
|
|
|
|
|
// Each is answered only when the target carries it, so the view keeps the shape of
|
|
|
|
|
// what it wraps instead of growing a member the collection does not have.
|
|
|
|
|
if ((prop === "size" || prop === "length") && prop in target) return kept(target).length;
|
|
|
|
|
// AN INDEX, on the array form. It names a position in the readable items, not in the
|
|
|
|
|
// collection underneath: the two differ by exactly what this view removed.
|
|
|
|
|
{
|
|
|
|
|
const i = indexOf(prop);
|
|
|
|
|
if (i !== null) return kept(target)[i];
|
|
|
|
|
}
|
|
|
|
|
// A Set yields the item for both halves of a `[key, value]` pair; `DeepSignalSet`
|
|
|
|
|
// follows the same shape, so `keys`/`values`/`entries` are the Set contract.
|
|
|
|
|
if (prop === "values" || prop === "keys") return () => kept(target)[Symbol.iterator]();
|
|
|
|
|
if (prop === "entries") return () => kept(target).map((i) => [i, i] as const)[Symbol.iterator]();
|
|
|
|
|
if (prop === "forEach") {
|
|
|
|
|
return (cb: (v: unknown, v2: unknown, s: unknown) => void) => {
|
|
|
|
|
for (const item of kept(target)) cb(item, item, receiver);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
// `has(item)` is filtered, not forwarded: the caller already holds the item, so the
|
|
|
|
|
// answer discloses nothing new — but upstream an unreadable item is never delivered
|
|
|
|
|
// at all, so "yes it is in there" would be an answer the target cannot give.
|
|
|
|
|
if (prop === "has") return (item: unknown) => keep(item) && (target as Set<unknown>).has(item);
|
|
|
|
|
// The reactive-set extras: they iterate, so they must iterate the filtered items.
|
|
|
|
|
// The list is `iteratorHelperKeys` from `@ng-org/alien-deepsignals` — an earlier
|
|
|
|
|
// pass whitelisted half of it and threw on the rest, so a holder's calls on their
|
|
|
|
|
// OWN data crashed (`toArray`, `reduce`, `first`…). Filtering is the answer for all
|
|
|
|
|
// of them; refusing is only for what is not on this list.
|
|
|
|
|
if (prop === "map") return (fn: (v: unknown, i: number) => unknown) => kept(target).map(fn);
|
|
|
|
|
if (prop === "filter") return (fn: (v: unknown, i: number) => boolean) => kept(target).filter(fn);
|
|
|
|
|
if (prop === "find") return (fn: (v: unknown, i: number) => boolean) => kept(target).find(fn);
|
|
|
|
|
if (prop === "some") return (fn: (v: unknown, i: number) => boolean) => kept(target).some(fn);
|
|
|
|
|
if (prop === "every") return (fn: (v: unknown, i: number) => boolean) => kept(target).every(fn);
|
|
|
|
|
if (prop === "toArray") return () => kept(target);
|
|
|
|
|
if (prop === "first") return () => kept(target)[0];
|
|
|
|
|
if (prop === "take") return (n: number) => kept(target).slice(0, n);
|
|
|
|
|
if (prop === "drop") return (n: number) => kept(target).slice(n);
|
|
|
|
|
if (prop === "flatMap") return (fn: (v: unknown, i: number) => unknown) => kept(target).flatMap(fn as never);
|
|
|
|
|
if (prop === "reduce") {
|
|
|
|
|
return (fn: (acc: unknown, v: unknown, i: number) => unknown, init?: unknown) =>
|
|
|
|
|
init === undefined
|
|
|
|
|
? kept(target).reduce(fn as never)
|
|
|
|
|
: kept(target).reduce(fn as never, init);
|
|
|
|
|
}
|
|
|
|
|
if (prop === "getById" || prop === "getBy") {
|
|
|
|
|
const inner = Reflect.get(target, prop, target) as ((...a: unknown[]) => unknown) | undefined;
|
|
|
|
|
if (typeof inner !== "function") return inner;
|
|
|
|
|
return (...args: unknown[]) => {
|
|
|
|
|
const item = inner.apply(target, args);
|
|
|
|
|
return keep(item) ? item : undefined;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MUTATIONS forward untouched. They take an item and return void or a boolean, so
|
|
|
|
|
// they yield nothing to leak — and the view must not break writes or the underlying
|
|
|
|
|
// reactivity. (Caught by `test/read-filter.test.ts` when the blanket refusal below
|
|
|
|
|
// was first written: refusing everything unknown also refused `add`.)
|
|
|
|
|
if (prop === "add" || prop === "delete" || prop === "clear") {
|
|
|
|
|
const fn = Reflect.get(target, prop, target);
|
|
|
|
|
return typeof fn === "function" ? fn.bind(target) : fn;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RAW ESCAPE HATCHES. `DeepSignalSet` exposes the underlying collection on
|
|
|
|
|
// dunder keys (`__raw__`, `__meta__` — `RAW_KEY` in `@ng-org/alien-deepsignals`),
|
|
|
|
|
// and the header used to claim "a plain property carries no items". It does here:
|
|
|
|
|
// `view.__raw__` handed back the unfiltered Set, every identity's items in it.
|
|
|
|
|
// Found by re-running the adversary on the fix (2026-08-07). Any dunder key is
|
|
|
|
|
// refused, because that is the convention the escape hatches follow.
|
|
|
|
|
if (typeof prop === "string" && prop.startsWith("__")) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`[ng-eventually] read filter: \`${prop}\` reaches past the view to the raw ` +
|
|
|
|
|
"collection, which holds every identity's items. There is no filtered form of it.",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ANY OTHER MEMBER, function or not: refuse rather than forward, unless the value
|
|
|
|
|
// behind it cannot reach an item at all. See the header — forwarding is a silent
|
|
|
|
|
// leak, and this view's one job is that it cannot show more than the holder may read.
|
|
|
|
|
return answerUnnamedMember(target, prop, "the collection view");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return new Proxy(set, {
|
|
|
|
|
get(target, prop, receiver) {
|
|
|
|
|
if (prop === Symbol.iterator) return function* () { yield* kept(target); };
|
|
|
|
|
// HOW MANY, under both spellings. `data` is a `DeepSignalSet` for an unordered read
|
|
|
|
|
// and an array-like `DeepSignal<ReadOnlyArray>` once the config asks for an
|
|
|
|
|
// `orderBy`, and `length` is a number - so it used to pass as a primitive and
|
|
|
|
|
// publish the UNFILTERED count, which is showing more than the holder may read.
|
|
|
|
|
// Each is answered only when the target carries it, so the view keeps the shape of
|
|
|
|
|
// what it wraps instead of growing a member the collection does not have.
|
|
|
|
|
if ((prop === "size" || prop === "length") && prop in target) return kept(target).length;
|
|
|
|
|
// A Set yields the item for both halves of a `[key, value]` pair; `DeepSignalSet`
|
|
|
|
|
// follows the same shape, so `keys`/`values`/`entries` are the Set contract.
|
|
|
|
|
if (prop === "values" || prop === "keys") return () => kept(target)[Symbol.iterator]();
|
|
|
|
|
if (prop === "entries") return () => kept(target).map((i) => [i, i] as const)[Symbol.iterator]();
|
|
|
|
|
if (prop === "forEach") {
|
|
|
|
|
return (cb: (v: unknown, v2: unknown, s: unknown) => void) => {
|
|
|
|
|
for (const item of kept(target)) cb(item, item, receiver);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
// `has(item)` is filtered, not forwarded: the caller already holds the item, so the
|
|
|
|
|
// answer discloses nothing new — but upstream an unreadable item is never delivered
|
|
|
|
|
// at all, so "yes it is in there" would be an answer the target cannot give.
|
|
|
|
|
if (prop === "has") return (item: unknown) => keep(item) && (target as Set<unknown>).has(item);
|
|
|
|
|
// The reactive-set extras: they iterate, so they must iterate the filtered items.
|
|
|
|
|
// The list is `iteratorHelperKeys` from `@ng-org/alien-deepsignals` — an earlier
|
|
|
|
|
// pass whitelisted half of it and threw on the rest, so a holder's calls on their
|
|
|
|
|
// OWN data crashed (`toArray`, `reduce`, `first`…). Filtering is the answer for all
|
|
|
|
|
// of them; refusing is only for what is not on this list.
|
|
|
|
|
if (prop === "map") return (fn: (v: unknown, i: number) => unknown) => kept(target).map(fn);
|
|
|
|
|
if (prop === "filter") return (fn: (v: unknown, i: number) => boolean) => kept(target).filter(fn);
|
|
|
|
|
if (prop === "find") return (fn: (v: unknown, i: number) => boolean) => kept(target).find(fn);
|
|
|
|
|
if (prop === "some") return (fn: (v: unknown, i: number) => boolean) => kept(target).some(fn);
|
|
|
|
|
if (prop === "every") return (fn: (v: unknown, i: number) => boolean) => kept(target).every(fn);
|
|
|
|
|
if (prop === "toArray") return () => kept(target);
|
|
|
|
|
if (prop === "first") return () => kept(target)[0];
|
|
|
|
|
if (prop === "take") return (n: number) => kept(target).slice(0, n);
|
|
|
|
|
if (prop === "drop") return (n: number) => kept(target).slice(n);
|
|
|
|
|
if (prop === "flatMap") return (fn: (v: unknown, i: number) => unknown) => kept(target).flatMap(fn as never);
|
|
|
|
|
if (prop === "reduce") {
|
|
|
|
|
return (fn: (acc: unknown, v: unknown, i: number) => unknown, init?: unknown) =>
|
|
|
|
|
init === undefined
|
|
|
|
|
? kept(target).reduce(fn as never)
|
|
|
|
|
: kept(target).reduce(fn as never, init);
|
|
|
|
|
}
|
|
|
|
|
if (prop === "getById" || prop === "getBy") {
|
|
|
|
|
const inner = Reflect.get(target, prop, target) as ((...a: unknown[]) => unknown) | undefined;
|
|
|
|
|
if (typeof inner !== "function") return inner;
|
|
|
|
|
return (...args: unknown[]) => {
|
|
|
|
|
const item = inner.apply(target, args);
|
|
|
|
|
return keep(item) ? item : undefined;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MUTATIONS forward untouched. They take an item and return void or a boolean, so
|
|
|
|
|
// they yield nothing to leak — and the view must not break writes or the underlying
|
|
|
|
|
// reactivity. (Caught by `test/read-filter.test.ts` when the blanket refusal below
|
|
|
|
|
// was first written: refusing everything unknown also refused `add`.)
|
|
|
|
|
if (prop === "add" || prop === "delete" || prop === "clear") {
|
|
|
|
|
const fn = Reflect.get(target, prop, target);
|
|
|
|
|
return typeof fn === "function" ? fn.bind(target) : fn;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RAW ESCAPE HATCHES. `DeepSignalSet` exposes the underlying collection on
|
|
|
|
|
// dunder keys (`__raw__`, `__meta__` — `RAW_KEY` in `@ng-org/alien-deepsignals`),
|
|
|
|
|
// and the header used to claim "a plain property carries no items". It does here:
|
|
|
|
|
// `view.__raw__` handed back the unfiltered Set, every identity's items in it.
|
|
|
|
|
// Found by re-running the adversary on the fix (2026-08-07). Any dunder key is
|
|
|
|
|
// refused, because that is the convention the escape hatches follow.
|
|
|
|
|
if (typeof prop === "string" && prop.startsWith("__")) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`[ng-eventually] read filter: \`${prop}\` reaches past the view to the raw ` +
|
|
|
|
|
"collection, which holds every identity's items. There is no filtered form of it.",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ANY OTHER MEMBER, function or not: refuse rather than forward, unless the value
|
|
|
|
|
// behind it cannot reach an item at all. See the header — forwarding is a silent
|
|
|
|
|
// leak, and this view's one job is that it cannot show more than the holder may read.
|
|
|
|
|
// An index on an array-like `data` lands here and is refused: it hands back a raw
|
|
|
|
|
// item, and there is no projection of it that this file would not have to invent.
|
|
|
|
|
return answerUnnamedMember(target, prop, "the collection view");
|
|
|
|
|
get: (target, prop, receiver) => read(target, prop, receiver),
|
|
|
|
|
getOwnPropertyDescriptor(target, prop) {
|
|
|
|
|
// An index past what may be read names no position of this view, and saying so is
|
|
|
|
|
// what keeps `has`, `ownKeys` and the descriptor telling one story.
|
|
|
|
|
const i = indexOf(prop);
|
|
|
|
|
if (i !== null && i >= kept(target).length) return undefined;
|
|
|
|
|
return filteredDescriptor(target, prop, () => read(target, prop, target));
|
|
|
|
|
},
|
|
|
|
|
has(target, prop) {
|
|
|
|
|
const i = indexOf(prop);
|
|
|
|
|
return i === null ? Reflect.has(target, prop) : i < kept(target).length;
|
|
|
|
|
},
|
|
|
|
|
ownKeys(target) {
|
|
|
|
|
// A Set holds its items internally and owns no key naming one, so there is nothing
|
|
|
|
|
// to project: `Object.keys` on it is already empty (measured). An array owns one key
|
|
|
|
|
// per position, and forwarding those published HOW MANY items the collection holds
|
|
|
|
|
// while `length` said how many may be read - the two disagreed by the leak.
|
|
|
|
|
if (!arrayLike) return Reflect.ownKeys(target);
|
|
|
|
|
const keys: Array<string | symbol> = [];
|
|
|
|
|
for (let i = 0; i < kept(target).length; i++) keys.push(String(i));
|
|
|
|
|
// Everything that is not a position passes through, so a key the target cannot hide
|
|
|
|
|
// - an array's own `length` - is never dropped.
|
|
|
|
|
for (const k of Reflect.ownKeys(target)) if (indexOf(k) === null) keys.push(k);
|
|
|
|
|
return keys;
|
|
|
|
|
},
|
|
|
|
|
}) as S;
|
|
|
|
|
}
|
|
|
|
@@ -311,40 +399,47 @@ function filteredPromise(value: PromiseLike<unknown>, caps: CapRegistry): Promis
|
|
|
|
|
*/
|
|
|
|
|
// @provenance makeReadFilteredShapeResult kind=aligned level=3 ref=sdk/js/orm/src/frontendAdapters/react/useShape.ts:useShape — the same capability filter, over the result envelope that hook returns rather than over the collection inside it; removed at migration
|
|
|
|
|
export function makeReadFilteredShapeResult<R extends object>(result: R, caps: CapRegistry): R {
|
|
|
|
|
/** What the view answers for one member. The `get` trap and the descriptor trap BOTH
|
|
|
|
|
* call this, so neither can answer what the other refuses - `data` used to come back
|
|
|
|
|
* RAW through `Object.getOwnPropertyDescriptor`, and so did the handle `get` throws on. */
|
|
|
|
|
const read = (target: object, prop: string | symbol): unknown => {
|
|
|
|
|
// THE ITEMS, under the three names alpha.21 offers them. Each is filtered rather
|
|
|
|
|
// than refused: a holder reading their own data through whichever member the SDK
|
|
|
|
|
// gives them is exactly what this view exists to keep working.
|
|
|
|
|
if (prop === "data") {
|
|
|
|
|
const d = Reflect.get(target, prop, target);
|
|
|
|
|
return cannotReachAnItem(d) ? d : filteredCollection(d as object, caps);
|
|
|
|
|
}
|
|
|
|
|
if (prop === "promise") {
|
|
|
|
|
const p = Reflect.get(target, prop, target);
|
|
|
|
|
return cannotReachAnItem(p) ? p : filteredPromise(p as PromiseLike<unknown>, caps);
|
|
|
|
|
}
|
|
|
|
|
if (prop === "subscription") {
|
|
|
|
|
const s = Reflect.get(target, prop, target);
|
|
|
|
|
// `undefined` when the caller passed no config: nothing was opened, so there is
|
|
|
|
|
// nothing to refuse, and answering the SDK's own value keeps that case honest.
|
|
|
|
|
if (cannotReachAnItem(s)) return s;
|
|
|
|
|
throw new Error(
|
|
|
|
|
"[ng-eventually] read filter: `subscription` hands back NextGraph's own " +
|
|
|
|
|
"`RdfOrmSubscription`, which reads and writes with no capability filter at all " +
|
|
|
|
|
"- one wallet hosts every identity here, so it would return every identity's " +
|
|
|
|
|
"objects. This package does not publish it. Read through `data`, `promise`, or " +
|
|
|
|
|
"`useShapeQuery`.",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
// THE PAGE COMMANDS. They return `void` and yield nothing, so they forward, exactly
|
|
|
|
|
// as `add`/`delete`/`clear` do on the collection view. What they load lands in the
|
|
|
|
|
// raw collection, which is only ever read back through the filter above.
|
|
|
|
|
if (prop === "nextPage" || prop === "previousPage") {
|
|
|
|
|
const fn = Reflect.get(target, prop, target);
|
|
|
|
|
return typeof fn === "function" ? fn.bind(target) : fn;
|
|
|
|
|
}
|
|
|
|
|
return answerUnnamedMember(target, prop, "the result envelope");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return new Proxy(result, {
|
|
|
|
|
get(target, prop) {
|
|
|
|
|
// THE ITEMS, under the three names alpha.21 offers them. Each is filtered rather
|
|
|
|
|
// than refused: a holder reading their own data through whichever member the SDK
|
|
|
|
|
// gives them is exactly what this view exists to keep working.
|
|
|
|
|
if (prop === "data") {
|
|
|
|
|
const d = Reflect.get(target, prop, target);
|
|
|
|
|
return cannotReachAnItem(d) ? d : filteredCollection(d as object, caps);
|
|
|
|
|
}
|
|
|
|
|
if (prop === "promise") {
|
|
|
|
|
const p = Reflect.get(target, prop, target);
|
|
|
|
|
return cannotReachAnItem(p) ? p : filteredPromise(p as PromiseLike<unknown>, caps);
|
|
|
|
|
}
|
|
|
|
|
if (prop === "subscription") {
|
|
|
|
|
const s = Reflect.get(target, prop, target);
|
|
|
|
|
// `undefined` when the caller passed no config: nothing was opened, so there is
|
|
|
|
|
// nothing to refuse, and answering the SDK's own value keeps that case honest.
|
|
|
|
|
if (cannotReachAnItem(s)) return s;
|
|
|
|
|
throw new Error(
|
|
|
|
|
"[ng-eventually] read filter: `subscription` hands back NextGraph's own " +
|
|
|
|
|
"`RdfOrmSubscription`, which reads and writes with no capability filter at all " +
|
|
|
|
|
"- one wallet hosts every identity here, so it would return every identity's " +
|
|
|
|
|
"objects. This package does not publish it. Read through `data`, `promise`, or " +
|
|
|
|
|
"`useShapeQuery`.",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
// THE PAGE COMMANDS. They return `void` and yield nothing, so they forward, exactly
|
|
|
|
|
// as `add`/`delete`/`clear` do on the collection view. What they load lands in the
|
|
|
|
|
// raw collection, which is only ever read back through the filter above.
|
|
|
|
|
if (prop === "nextPage" || prop === "previousPage") {
|
|
|
|
|
const fn = Reflect.get(target, prop, target);
|
|
|
|
|
return typeof fn === "function" ? fn.bind(target) : fn;
|
|
|
|
|
}
|
|
|
|
|
return answerUnnamedMember(target, prop, "the result envelope");
|
|
|
|
|
},
|
|
|
|
|
get: (target, prop) => read(target, prop),
|
|
|
|
|
getOwnPropertyDescriptor: (target, prop) =>
|
|
|
|
|
filteredDescriptor(target, prop, () => read(target, prop)),
|
|
|
|
|
}) as R;
|
|
|
|
|
}
|
|
|
|
|