/** * The ORM family this package offers an application - the SDK's one-shot read and its * subscription-free write, each WRAPPED at the same name and the same signature. * * -- Why wrapped, and never re-exported ------------------------------------ * `export { getObjects } from "@ng-org/orm"` would compile and would look like the most * faithful thing available. It is the opposite: it hands the caller a path straight to * the SDK that SHORT-CIRCUITS the emulation - the capability filter, and above all the * shared-wallet identity boundary, which is the whole of what this package protects. One * wallet hosts every identity here, so an unwrapped ORM read returns EVERY identity's * data with nothing signalling it. Everything offered from this family is therefore * wrapped; a symbol that cannot be wrapped meaningfully is not published at all. * * -- What each wrapper adds, and where the target does the same thing ------ * A READ is filtered on what comes back. `emulated-verifier/read-filter.ts` keeps only * items whose document - their `@graph` - the current holder holds a cap for, which is * the emulation of what the broker bounds at delivery, applied per document, exactly * where `surface/use-shape.ts` and `surface/use-shape-query.ts` apply it. * * A WRITE is NOT filtered, and the asymmetry is upstream's rather than ours: a write is * bounded by write capability - `verify_permission`, reachable only from `Commit::verify`, * so it fires on commits and never on reads - and nothing is hidden from the writer. So * `insertObject` takes a GUARD BEFORE the write (`emulated-verifier/reach.ts`), the same * pair `docs.sparqlUpdate` takes, and never a filter over what it returns. * * -- The second door the guard needs, and where it is ---------------------- * These two wrappers guard the calls an application MAKES. They do not, and cannot, guard * a mutation of a live signal object - `set.add(...)`, `set.delete(...)` on what a shape * query handed back - because that write is made by the ORM itself, batched into * `ng.graph_orm_update`. That door is guarded where every other `ng` call is, in * `surface/ng-proxy.ts`, and `surface/lifecycle.ts` is what makes sure the ORM is holding * the wrapped `ng` when it knocks on it. * * -- What is deliberately NOT published from this family ------------------- * `@ng-org/orm`'s entry also exports `RdfOrmSubscription`, `DiscreteOrmSubscription`, * `ngSession`, and `getRaw` / `watch` / `effect` re-exported from * `@ng-org/alien-deepsignals`. None of them is published here, and each for a reason a * later reader can check rather than a preference: * * - `RdfOrmSubscription` - an application never holds a subscription: it hands the * shape query to `configure` and reads `useShapeQuery`'s snapshot back (see the note * on `OrmSubscriptionHandle` in `model/types.ts`). And `getOrCreate` POOLS on a key * built from the shape and the scope alone - no identity in it - so a second virtual * user asking the same question is handed the FIRST one's live instance. What makes * that safe here is the filter this package applies on the way out; publishing the * handle would hand out the instance instead. * - `DiscreteOrmSubscription` - a JSON-CRDT document. Its items carry no `@graph`, so * the read filter has nothing to read, and its writes leave through * `discrete_orm_update`, a third channel nothing here guards. Nothing in this package * reads or writes a discrete document; publishing it would open a surface and a hole * in the same gesture. * - `ngSession` - a module-level PROMISE, not a function, and what it resolves to is * the SDK handle plus the SESSION. Here one physical session hosts every virtual * identity, so handing it out hands every identity the same one; the contract already * says the session is the package's. Wrapping a resolved value would also force a * RUNTIME `@ng-org/orm` import into this module, which is the one thing the injection * discipline exists to avoid. * - `getRaw` - it returns the object BEHIND a signal, which is the same escape hatch * `read-filter.ts` refuses on a dunder key (`__raw__`): it hands back the unfiltered * collection, every identity's items in it. There is no filtered form of it. * - `watch` / `effect` - `@ng-org/alien-deepsignals`' reactivity primitives, not the * ORM's. They carry no document and grant nothing, so there is nothing to wrap; an * application that wants them declares that package. */ import { getCaps, getConfig } from "../shared-wallet/bootstrap"; import { filterReadable } from "../emulated-verifier/read-filter"; import { assertMayReach, assertMayWrite } from "../emulated-verifier/reach"; import { isNuri } from "../model/nuri"; import type { GetObjectsLike, InsertObjectLike, Nuri } from "../model/types"; // The SDK's own types, imported as TYPES (erased at build, so no runtime `@ng-org` // import is added here). The provenance of what is PUBLISHED is recorded in `index.ts`. import type { RdfOrmConfig } from "@ng-org/orm/all"; import type { BaseType, ShapeType } from "@ng-org/shex-orm"; /** * The injected real function, or the error that says it was never handed over. * * Upstream an application imports the ORM; here it hands it to `configure`, like the * `ng`, the `useShape` and the shape query beside it. Absent, there is nothing to call * through to - and that has to be said out loud, naming the field to pass, rather than * answered with an empty result. */ function injected( name: K, ): NonNullable[K]> { const f = getConfig()[name]; if (!f) { throw new Error( `[ng-eventually] ${name}: not injected. Pass \`${name}\` (from \`@ng-org/orm\`) to ` + "configure() - this package wraps the SDK's own call and has none of its own to " + "fall back on.", ); } return f; } /** * The document an ORM object declares it lives in, or `null` when it names none. * * `@graph` is an untyped value on a property bag (`BaseType extends Record`), * so it is NARROWED here rather than cast - the same door `read-filter.ts` reads items * through, and for the same reason: anything that is not a NextGraph reference names no * document. */ function graphOf(object: unknown): Nuri | null { const g = (object as Record | null)?.["@graph"]; return typeof g === "string" && isNuri(g) ? g : null; } /** * Read objects of one shape in one scope, ONCE - the SDK's own call, with the read * filter applied to what comes back. * * Both parameters are upstream's, passed through untouched: the {@link ShapeType} and the * config it declares minus the two pagination fields a one-shot read has no use for. What * comes back is upstream's too, and DETACHED - a `structuredClone` of the raw signal * object - so an ARRAY when the config asks for an `orderBy` and a `Set` otherwise, and * mutating an item of it reaches nothing. Deleting an item of it removes nothing either, * and reports nothing: removal is a mutation of the LIVE set a subscription holds. * * The filter keeps the shape of the collection it was given: a `Set` in is a `Set` out. * Before the first cap is issued the emulation is not in force and everything passes * through, exactly as in `surface/use-shape.ts`. */ // @provenance getObjects kind=passthrough level=3 ref=sdk/js/orm/src/connector/getObjects.ts:getObjects - forwards to the injected real function; the read-filtered result emulates the subset the broker will deliver. The cited revision takes `scope: Scope | string`; the published `@ng-org/orm@0.1.2-alpha.21` widened that parameter to the config below, which is what a consumer compiles against and therefore what is mirrored here export async function getObjects( shapeType: ShapeType, config: Omit, "pageSize" | "maxActivePages">, ): Promise> { const read = injected("getObjects") as GetObjectsLike; const objects = await read(shapeType, config); const caps = getCaps(); // No cap issued yet -> the emulation is not in force and the result passes through. if (!caps.isEnforcing()) return objects; const kept = filterReadable(objects, caps); return Array.isArray(objects) ? kept : new Set(kept); } /** * Insert one ORM-typed object, without opening a subscription for it - the SDK's own * call, behind the write guard. * * The object must carry its `@graph`: upstream the insert subscribes to NO document, so * that field is the only thing that says where the write lands, and an object without one * makes the ORM throw. It is also what this guard reads, which is why the two agree * without either of them being bent to the other. * * Two questions are asked, and they are not the same one - the pair `docs.sparqlUpdate` * asks. Reaching is POSSESSION of the document's read cap. Writing is OWNERSHIP: upstream * the right to write is membership of the repo, and how you came by the read key bears on * it not at all - a public store serves its read cap to whoever asks, and a cap deposited * in your inbox is one someone gave you. Neither makes you a writer. * * The refusal happens BEFORE the call, so an unauthorized write is not attempted rather * than attempted and caught. An object naming no document is forwarded untouched: there * is nothing here to judge, and the ORM's own refusal is the one that should be heard. */ // @provenance insertObject kind=passthrough level=3 ref=sdk/js/orm/src/connector/insertObject.ts:insertObject - forwards to the injected real function; the guard before it emulates the write capability the target checks on the commit export async function insertObject( shapeType: ShapeType, object: T, ): Promise { const write = injected("insertObject") as InsertObjectLike; const graph = graphOf(object); if (graph !== null) { assertMayReach(graph, "insertObject"); await assertMayWrite(graph, "insertObject"); } return write(shapeType, object); }