/** * orm-fake - a stand-in for the SDK's shape query, `RdfOrmSubscription.getOrCreate`. * * Not a `*.test.ts`, so `bun test` does not pick it up: it is the injection two suites * share, and it is the seam the package is built on - an application hands `configure` * the real ORM, and a suite hands it this. * * -- Why a small ENGINE and not a canned answer ---------------------------- * `useShapeQuery` no longer interprets a scope: `graphs`, `subjects` and the * whole-dataset marker go to the engine verbatim. A fake that ignored them and returned * a fixed list would make every scope test green while proving nothing - the failure * mode `test-the-case-space` is about. So this answers the way * `schema_shape_to_sparql` / `query_quads_for_shape` do, and the three readings of the * axes are real here: * * - `graphs: []` (or absent) -> NO objects. `graphs` carrying `did:ng:i` or `""` -> * every other entry is discarded and the graph filter drops entirely. Otherwise the * filter is LITERAL - a graph is in the list or it is not, no prefix matching. * - `subjects: []` -> no filtering; otherwise literal, over ROOT subjects. * - the shape -> every predicate of `schema[shape]` must be present on the subject, * and a predicate carrying `literals` must match one of them. The object is * projected under each predicate's `readablePredicate`, plus `@id` and `@graph`. * * -- What it does NOT invent ----------------------------------------------- * Readiness resolves on ONE path - a delivery - exactly as upstream's `resolveReady` * runs only on the arrival of initial data. Nothing here ever REJECTS `readyPromise`, * because nothing upstream does: `orm_start_graph` failing is caught and logged, and a * refused read is an eternal pending. A suite that wants the pending-for-ever state * asks for it by not delivering, which is the state the real thing produces. * * {@link FakeOrm.refuse} is the other real failure: `getOrCreate` THROWS at the call. */ import type { OrmSubscriptionHandle, OrmSubscriptionLike } from "../src/model/types"; import type { Scope } from "@ng-org/orm"; import type { BaseType, Predicate, ShapeType } from "@ng-org/shex-orm"; /** Upstream's whole-dataset marker; `""` means the same thing. */ const ENTIRE_DATASET = "did:ng:i"; /** One triple in the dataset this fake engine answers from. */ export interface OrmQuad { graph: string; subject: string; predicate: string; object: string; } type Listener = (changes: { adds: unknown[]; updates: unknown[]; removes: unknown[] }) => void; interface OpenQuery { shape: ShapeType; scope: Scope; listeners: Set; items: BaseType[]; delivered: boolean; closed: boolean; resolveReady: () => void; } export interface FakeOrm { /** What goes into `configure({ ormSubscription })`. */ query: OrmSubscriptionLike; /** Add a triple to the dataset. After a delivery this also pushes, as an engine patch * reaching the signal object does. */ seed(graph: string, subject: string, predicate: string, object: string): void; /** Deliver the initial payload to every query still waiting for one - what resolves * readiness upstream, and the only thing that does. */ deliver(): void; /** Make every SUBSEQUENT `getOrCreate` throw. The one failure this path really has. */ refuse(message: string): void; /** Stop refusing. */ answer(): void; /** The scopes this fake was asked for, in order - so a suite can assert the scope * reached the SDK exactly as the caller wrote it. */ asked: Array<{ shape: string; scope: Scope }>; /** How many queries are open right now (`close()` decrements). */ openCount(): number; } /** The literal values a predicate constrains its object to, if any. */ function literalsOf(p: Predicate): string[] { const out: string[] = []; for (const dt of p.dataTypes ?? []) { for (const lit of dt.literals ?? []) if (typeof lit === "string") out.push(lit); } return out; } /** Does `subject` in `graph` satisfy every predicate of the shape? If so, the object. */ function project( quads: OrmQuad[], graph: string, subject: string, predicates: Predicate[], ): BaseType | null { const own = quads.filter((q) => q.graph === graph && q.subject === subject); const out: Record = { "@id": subject, "@graph": graph }; for (const p of predicates) { const wanted = literalsOf(p); const values = own .filter((q) => q.predicate === p.iri) .map((q) => q.object) .filter((o) => wanted.length === 0 || wanted.includes(o)); if (values.length === 0) return null; // a required triple pattern with no match out[p.readablePredicate] = values.length === 1 ? values[0] : values; } return out as BaseType; } /** The objects the engine would return for one shape in one scope. */ function answerFor(quads: OrmQuad[], shape: ShapeType, scope: Scope): BaseType[] { const graphs = (typeof scope.graphs === "string" ? [scope.graphs] : (scope.graphs ?? [])).map( (g) => (g === "" ? ENTIRE_DATASET : g), ); if (graphs.length === 0) return []; // upstream: no objects are returned const everywhere = graphs.includes(ENTIRE_DATASET); const subjects = scope.subjects ?? []; const predicates = shape.schema[shape.shape]?.predicates ?? []; const seen = new Set(); const out: BaseType[] = []; for (const q of quads) { if (!everywhere && !graphs.includes(q.graph)) continue; if (subjects.length > 0 && !subjects.includes(q.subject)) continue; const key = q.graph + "/" + q.subject; if (seen.has(key)) continue; seen.add(key); const obj = project(quads, q.graph, q.subject, predicates); if (obj !== null) out.push(obj); } return out; } /** * A fake shape query over an in-memory dataset. * * `autoDeliver` (the default) hands every query its initial payload on the next tick, * which is the convenient mode for suites that are not about readiness. Turn it off and * call {@link FakeOrm.deliver} to hold the pending state open. */ export function makeOrm( options: { autoDeliver?: boolean; source?: () => OrmQuad[] } = {}, ): FakeOrm { const quads: OrmQuad[] = []; /** Where the engine reads from - its own dataset, or one a suite already holds (the * fake wallet's, so the objects read back are the ones the surface really wrote). */ const dataset = options.source ?? (() => quads); const open = new Set(); const asked: Array<{ shape: string; scope: Scope }> = []; const autoDeliver = options.autoDeliver !== false; let refusal: string | null = null; function recompute(q: OpenQuery): void { if (!q.delivered || q.closed) return; q.items = answerFor(dataset(), q.shape, q.scope); for (const l of q.listeners) l({ adds: q.items, updates: [], removes: [] }); } function deliverOne(q: OpenQuery): void { if (q.delivered || q.closed) return; q.delivered = true; q.items = answerFor(dataset(), q.shape, q.scope); q.resolveReady(); for (const l of q.listeners) l({ adds: q.items, updates: [], removes: [] }); } const query = (( shapeType: ShapeType, scope: Scope, ): OrmSubscriptionHandle => { if (refusal !== null) throw new Error(refusal); const shape = shapeType as unknown as ShapeType; asked.push({ shape: shape.shape, scope }); let resolveReady!: () => void; const readyPromise = new Promise((r) => (resolveReady = r)); const record: OpenQuery = { shape, scope, listeners: new Set(), items: [], delivered: false, closed: false, resolveReady, }; open.add(record); if (autoDeliver) setTimeout(() => deliverOne(record), 0); return { readyPromise, get signalObject(): Iterable { return record.items; }, addChangeListener(listener: Listener): void { record.listeners.add(listener); }, removeChangeListener(listener: Listener): void { record.listeners.delete(listener); }, close(): void { record.closed = true; record.listeners.clear(); open.delete(record); }, }; }) as OrmSubscriptionLike; return { query, seed(graph, subject, predicate, object): void { quads.push({ graph, subject, predicate, object }); for (const q of open) recompute(q); }, deliver(): void { for (const q of [...open]) deliverOne(q); }, refuse(message): void { refusal = message; }, answer(): void { refusal = null; }, asked, openCount: () => open.size, }; }