feat!: useShapeQuery lit par la requete de forme du SDK
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* 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<BaseType>;
|
||||
scope: Scope;
|
||||
listeners: Set<Listener>;
|
||||
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<string, unknown> = { "@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<BaseType>, 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<string>();
|
||||
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<OpenQuery>();
|
||||
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 = (<T extends BaseType>(
|
||||
shapeType: ShapeType<T>,
|
||||
scope: Scope,
|
||||
): OrmSubscriptionHandle => {
|
||||
if (refusal !== null) throw new Error(refusal);
|
||||
const shape = shapeType as unknown as ShapeType<BaseType>;
|
||||
asked.push({ shape: shape.shape, scope });
|
||||
let resolveReady!: () => void;
|
||||
const readyPromise = new Promise<void>((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<unknown> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user