rm obsolete ng-mock

feat/orm
Laurin Weger 1 week ago
parent 43c3356942
commit 2dc6136d51
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 6
      sdk/ng-sdk-js/examples/multi-framework-signals/package.json
  2. 3
      sdk/ng-sdk-js/examples/multi-framework-signals/src/frontends/react/HelloWorld.tsx
  3. 217
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/tests/applyDiff.test.ts
  4. 56
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/tests/updatesWithWasm.test.ts
  5. 147
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/objectBuilder/buildObjects.ts
  6. 172
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/shapeHandler.ts
  7. 10
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/shapeManager.ts
  8. 220
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildConstruct.ts
  9. 140
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildSparqlConstructFromShape.ts
  10. 9
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/sparqlConstruct.test.ts
  11. 129
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/testShape.schema.ts
  12. 18
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/types.ts
  13. 22
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/updateShape.ts
  14. 235
      sdk/ng-sdk-js/ng-signals/src/connector/applyDiff.test.ts

@ -21,17 +21,13 @@
"vite-plugin-top-level-await": "^1.6.0",
"@astrojs/svelte": "7.1.0",
"@astrojs/vue": "^5.1.0",
"@gn8/alien-signals-react": "^0.1.1",
"@gn8/alien-signals-solid": "^0.1.1",
"@gn8/alien-signals-svelte": "^0.1.1",
"@gn8/alien-signals-vue": "^0.1.1",
"@nextgraph-monorepo/ng-alien-deepsignals": "workspace:*",
"@nextgraph-monorepo/ng-shex-orm": "workspace:*",
"@types/react": "19.1.10",
"@types/react-dom": "19.1.7",
"@types/shexj": "^2.1.7",
"alien-signals": "^2.0.7",
"astro": "5.13.2",
"astro": "5.14.4",
"install": "^0.13.0",
"npm": "^11.5.2",
"prettier-eslint": "^16.4.2",

@ -3,9 +3,6 @@ import { useShape } from "@nextgraph-monorepo/ng-signals/react";
import flattenObject from "../utils/flattenObject";
import { TestObjectShapeType } from "../../shapes/ldo/testShape.shapeTypes";
// Hack to get mock backend started
import { mockTestObject } from "../../ng-mock/wasm-land/shapeHandler";
export function HelloWorldReact() {
const state = useShape(TestObjectShapeType);

@ -1,217 +0,0 @@
import { describe, test, expect } from "vitest";
import {
applyDiff,
} from "@nextgraph-monorepo/ng-signals";
import type { Patch } from "@nextgraph-monorepo/ng-signals";
/**
* Build a patch path string from segments (auto-prefix /)
*/
function p(...segs: (string | number)[]) {
return "/" + segs.map(String).join("/");
}
describe("applyDiff - set operations (primitives)", () => {
test("add single primitive into new set", () => {
const state: any = {};
const diff: Patch[] = [
{ op: "add", type: "set", path: p("tags"), value: "a" },
];
applyDiff(state, diff);
expect(state.tags).toBeInstanceOf(Set);
expect([...state.tags]).toEqual(["a"]);
});
test("add multiple primitives into new set", () => {
const state: any = {};
const diff: Patch[] = [
{ op: "add", type: "set", path: p("nums"), value: [1, 2, 3] },
];
applyDiff(state, diff);
expect([...state.nums]).toEqual([1, 2, 3]);
});
test("add primitives merging into existing set", () => {
const state: any = { nums: new Set([1]) };
const diff: Patch[] = [
{ op: "add", type: "set", path: p("nums"), value: [2, 3] },
];
applyDiff(state, diff);
expect([...state.nums].sort()).toEqual([1, 2, 3]);
});
test("remove single primitive from set", () => {
const state: any = { tags: new Set(["a", "b"]) };
const diff: Patch[] = [
{ op: "remove", type: "set", path: p("tags"), value: "a" },
];
applyDiff(state, diff);
expect([...state.tags]).toEqual(["b"]);
});
test("remove multiple primitives from set", () => {
const state: any = { nums: new Set([1, 2, 3, 4]) };
const diff: Patch[] = [
{ op: "remove", type: "set", path: p("nums"), value: [2, 4] },
];
applyDiff(state, diff);
expect([...state.nums].sort()).toEqual([1, 3]);
});
});
describe("applyDiff - set operations (object sets)", () => {
test("add object entries to new object-set", () => {
const state: any = {};
const diff: Patch[] = [
{
op: "add",
type: "set",
path: p("users"),
value: { u1: { id: "u1", n: 1 }, u2: { id: "u2", n: 2 } },
},
];
applyDiff(state, diff);
expect(state.users.u1).toEqual({ id: "u1", n: 1 });
expect(state.users.u2).toEqual({ id: "u2", n: 2 });
});
test("merge object entries into existing object-set", () => {
const state: any = { users: { u1: { id: "u1", n: 1 } } };
const diff: Patch[] = [
{
op: "add",
type: "set",
path: p("users"),
value: { u2: { id: "u2", n: 2 } },
},
];
applyDiff(state, diff);
expect(Object.keys(state.users).sort()).toEqual(["u1", "u2"]);
});
test("remove object entries from object-set", () => {
const state: any = { users: { u1: {}, u2: {}, u3: {} } };
const diff: Patch[] = [
{ op: "remove", type: "set", path: p("users"), value: ["u1", "u3"] },
];
applyDiff(state, diff);
expect(Object.keys(state.users)).toEqual(["u2"]);
});
test("adding primitives to existing object-set replaces with Set", () => {
const state: any = { mixed: { a: {}, b: {} } };
const diff: Patch[] = [
{ op: "add", type: "set", path: p("mixed"), value: [1, 2] },
];
applyDiff(state, diff);
expect(state.mixed).toBeInstanceOf(Set);
expect([...state.mixed]).toEqual([1, 2]);
});
});
describe("applyDiff - object & literal operations", () => {
test("add object (create empty object)", () => {
const state: any = {};
const diff: Patch[] = [{ op: "add", path: p("address"), type: "object" }];
applyDiff(state, diff);
expect(state.address).toEqual({});
});
test("add nested object path with ensurePathExists", () => {
const state: any = {};
const diff: Patch[] = [
{ op: "add", path: p("a", "b", "c"), type: "object" },
];
applyDiff(state, diff, true);
expect(state.a.b.c).toEqual({});
});
test("add primitive value", () => {
const state: any = { address: {} };
const diff: Patch[] = [
{ op: "add", path: p("address", "street"), value: "1st" },
];
applyDiff(state, diff);
expect(state.address.street).toBe("1st");
});
test("overwrite primitive value", () => {
const state: any = { address: { street: "old" } };
const diff: Patch[] = [
{ op: "add", path: p("address", "street"), value: "new" },
];
applyDiff(state, diff);
expect(state.address.street).toBe("new");
});
test("remove primitive", () => {
const state: any = { address: { street: "1st", country: "Greece" } };
const diff: Patch[] = [{ op: "remove", path: p("address", "street") }];
applyDiff(state, diff);
expect(state.address.street).toBeUndefined();
expect(state.address.country).toBe("Greece");
});
test("remove object branch", () => {
const state: any = { address: { street: "1st" }, other: 1 };
const diff: Patch[] = [{ op: "remove", path: p("address") }];
applyDiff(state, diff);
expect(state.address).toBeUndefined();
expect(state.other).toBe(1);
});
});
describe("applyDiff - multiple mixed patches in a single diff", () => {
test("sequence of mixed set/object/literal add & remove", () => {
const state: any = {
users: { u1: { id: "u1" } },
tags: new Set(["old"]),
};
const diff: Patch[] = [
{ op: "add", type: "set", path: p("users"), value: { u2: { id: "u2" } } },
{ op: "add", path: p("profile"), type: "object" },
{ op: "add", path: p("profile", "name"), value: "Alice" },
{ op: "add", type: "set", path: p("tags"), value: ["new"] },
{ op: "remove", type: "set", path: p("tags"), value: "old" },
];
applyDiff(state, diff);
expect(Object.keys(state.users).sort()).toEqual(["u1", "u2"]);
expect(state.profile.name).toBe("Alice");
expect([...state.tags]).toEqual(["new"]);
});
test("complex nested path creation and mutations with ensurePathExists", () => {
const state: any = {};
const diff: Patch[] = [
{ op: "add", path: p("a", "b"), type: "object" },
{ op: "add", path: p("a", "b", "c"), value: 1 },
{ op: "add", type: "set", path: p("a", "nums"), value: [1, 2, 3] },
{ op: "remove", type: "set", path: p("a", "nums"), value: 2 },
{ op: "add", path: p("a", "b", "d"), value: 2 },
{ op: "remove", path: p("a", "b", "c") },
];
applyDiff(state, diff, true);
expect(state.a.b.c).toBeUndefined();
expect(state.a.b.d).toBe(2);
expect(state.a.nums).toBeInstanceOf(Set);
expect([...state.a.nums].sort()).toEqual([1, 3]);
});
});
describe("applyDiff - ignored / invalid scenarios", () => {
test("skip patch with non-leading slash path", () => {
const state: any = {};
const diff: Patch[] = [{ op: "add", path: "address/street", value: "x" }];
applyDiff(state, diff);
expect(state).toEqual({});
});
test("missing parent without ensurePathExists -> patch skipped and no mutation", () => {
const state: any = {};
const diff: Patch[] = [{ op: "add", path: p("a", "b", "c"), value: 1 }];
applyDiff(state, diff, false);
expect(state).toEqual({});
});
});
describe("applyDiff - ignored / invalid scenarios", () => {
test("skip patch with non-leading slash path", () => {
const state: any = {};
const diff: Patch[] = [{ op: "add", path: "address/street", value: "x" }];
applyDiff(state, diff);
expect(state).toEqual({});
});
test("missing parent without ensurePathExists -> patch skipped and no mutation", () => {
const state: any = {};
const diff: Patch[] = [{ op: "add", path: p("a", "b", "c"), value: 1 }];
applyDiff(state, diff, false);
expect(state).toEqual({});
});
});

@ -1,56 +0,0 @@
import { describe, expect, test } from "vitest";
import { createSignalObjectForShape } from "@nextgraph-monorepo/ng-signals";
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
describe("Signal modification and propagation to backend with or without signal pooling", () => {
for (const withPooling of [true, false]) {
test(`shape object notification comes back to others ${
withPooling ? "with" : "without"
} signal pooling`, async () => {
const object1 = createSignalObjectForShape(
"TestShape",
undefined,
withPooling
);
const object2 = createSignalObjectForShape(
"TestShape",
undefined,
withPooling
);
const object3 = createSignalObjectForShape(
"Shape2",
undefined,
withPooling
);
const object4 = createSignalObjectForShape(
"Shape2",
undefined,
withPooling
);
await wait(10);
// Update object 1 and expect object 2 to update as well.
// @ts-expect-error
object1.name = "Updated name from object1";
await wait(10);
// @ts-expect-error
expect(object2.name).toBe("Updated name from object1");
// Expect object of different shape not to have changed.
// @ts-expect-error
expect(object3.name).toBe("Niko's cat");
// Update object 4 and expect object 3 with same shape to have updated.
// @ts-expect-error
object4.name = "Updated name from object4";
await wait(10);
// @ts-expect-error
expect(object3!.name).toBe("Updated name from object4");
});
}
});

@ -1,147 +0,0 @@
import type {
ShapeType,
Shape,
Predicate,
Schema,
} from "@nextgraph-monorepo/ng-shex-orm";
interface Triple {
s: string;
p: string;
o: string;
}
interface TrackedSubject {
shape: Shape;
parentObject?: TrackedSubject;
valid?: boolean;
trackedPredicates: Record<string, TrackedPredicate>;
/** For Sub-objects only */
untracked?: boolean;
}
interface TrackedPredicate {
shape: Predicate;
currentCardinality: number;
childSubjects?: TrackedSubject[];
}
export function buildObjects(shapeType: ShapeType<any>) {
//
}
export function onTriplesRemoved(
trackedSubjects: Record<string, TrackedSubject[]>,
triplesRemoved: string[][],
shapeType: ShapeType<any>
) {
//
}
/**
* Adds new triples to tracked subjects and creates and returns
* new tracked (possibly nested) subjects.
*/
export function onTriplesAdded(
trackedSubjects: Record<string, TrackedSubject[]>,
triplesAdded: Triple[],
schema: Schema,
rootShape?: Shape
): { newTrackedSubjects: Record<string, TrackedSubject[]> } {
// Track for secondary iterations.
const newTrackedSubjectsCreated: Record<string, TrackedSubject[]> = {};
for (const triple of triplesAdded) {
if (!trackedSubjects[triple.s]) {
// If rootShape is not set, we are applying the triples new nested objects
// and we don't create new tracked subjects.
if (!rootShape) continue;
// Check if predicate is in root shape type.
const matchedPredicate = rootShape.predicates.find((p) => triple.p);
if (!matchedPredicate) {
// Nothing to track.
continue;
}
// The triple should be tracked. Create new tracked subject.
const newTrackedSubject: TrackedSubject = {
shape: rootShape,
trackedPredicates: {
[triple.p]: {
shape: matchedPredicate,
currentCardinality: 1,
},
},
};
trackedSubjects[triple.s] = [newTrackedSubject];
} else {
// Add triple to tracked subject(s).
// In the case of nested shapes, the same subject can be tracked
// in multiple shapes.
const trackedSubjectsMatching = trackedSubjects[triple.s];
for (const trackedSubject of trackedSubjectsMatching) {
// Is predicate tracked in this subject's shape?
const matchedPredShape = trackedSubject.shape.predicates.find(
(predShape) => predShape.iri === triple.p
);
if (!matchedPredShape) continue;
// Get or create tracked predicate for tracked shape.
let trackedPredicate =
trackedSubject.trackedPredicates[matchedPredShape?.iri];
if (!trackedSubjects) {
trackedPredicate = {
currentCardinality: 0,
shape: matchedPredShape,
};
}
// Increase cardinality.
trackedPredicate.currentCardinality += 1;
// If predicate shape has nested object, track that too.
if (trackedPredicate.shape.nestedShape) {
const newTrackedObject: TrackedSubject = {
shape: schema[matchedPredShape.nestedShape as string],
trackedPredicates: {},
parentObject: trackedSubject,
};
// Remember for adding to registry and for re-running on nested shapes.
const newTracked = newTrackedSubjectsCreated[triple.o];
if (!newTracked)
newTrackedSubjectsCreated[triple.o] = [
newTrackedObject,
];
newTracked.push(newTrackedObject);
// Link to parent
const childSubjects = trackedPredicate.childSubjects;
if (childSubjects) childSubjects.push(newTrackedObject);
else trackedPredicate.childSubjects = [newTrackedObject];
}
// TODO: Inform tracked subjects about change
}
}
}
// Rerun triples on new tracked subjects created.
// Then merge with parent tracked subjects
const newNestedSubjectsCreated = onTriplesAdded(
newTrackedSubjectsCreated,
triplesAdded,
schema
);
// TODO: Is it correct to do this?
const ret: Record<string, TrackedSubject[]> = {};
for (const subjectIri of Object.keys(newTrackedSubjectsCreated)) {
ret[subjectIri] = [
...newTrackedSubjectsCreated[subjectIri],
...newNestedSubjectsCreated.newTrackedSubjects[subjectIri],
];
}
// Update Valid invalid
return { newTrackedSubjects: ret };
}
// Do we have infinite-loop issues?

@ -1,172 +0,0 @@
import * as shapeManager from "./shapeManager";
import type { WasmConnection, Diff, Scope } from "./types";
import type { ShapeType, BaseType } from "@nextgraph-monorepo/ng-shex-orm";
import * as ng from "@nextgraph-monorepo/ng-sdk-js";
import type { Person } from "../../shapes/ldo/personShape.typings";
import type { Cat } from "../../shapes/ldo/catShape.typings";
import type { TestObject } from "../../shapes/ldo/testShape.typings";
import updateShape from "./updateShape";
import { testShapeSchema } from "../../shapes/ldo/testShape.schema";
import { TestObjectShapeType } from "../../shapes/ldo/testShape.shapeTypes";
// Messages exchanged over the BroadcastChannel("shape-manager")
interface WasmMessage {
type:
| "Request"
| "InitialResponse"
| "FrontendUpdate"
| "BackendUpdate"
| "Stop";
connectionId: string;
diff?: Diff;
shapeType?: ShapeType<any>;
initialData?: BaseType;
}
export const mockTestObject = {
id: "ex:mock-id-1",
type: "TestObject",
stringValue: "string",
numValue: 42,
boolValue: true,
arrayValue: [1, 2, 3],
objectValue: {
id: "urn:obj-1",
nestedString: "nested",
nestedNum: 7,
nestedArray: [10, 12],
},
anotherObject: {
"id:1": {
id: "id:1",
prop1: "prop1 value",
prop2: 100,
},
"id:2": {
id: "id:1",
prop1: "prop2 value",
prop2: 200,
},
},
} satisfies TestObject;
const mockShapeObject1 = {
id: "ex:person-1",
type: "Person",
name: "Bob",
address: {
id: "urn:person-home-1",
street: "First street",
houseNumber: "15",
},
hasChildren: true,
numberOfHouses: 0,
} satisfies Person;
const mockShapeObject2 = {
id: "ex:cat-1",
type: "Cat",
name: "Niko's cat",
age: 12,
numberOfHomes: 3,
address: {
id: "Nikos-cat-home",
street: "Niko's street",
houseNumber: "15",
floor: 0,
},
} satisfies Cat;
// Single BroadcastChannel for wasm-land side
const communicationChannel = new BroadcastChannel("shape-manager");
function getInitialObjectByShapeId<T extends BaseType>(shapeId?: string): T {
if (shapeId?.includes("TestObject")) return mockTestObject as unknown as T;
if (shapeId?.includes("Person")) return mockShapeObject1 as unknown as T;
if (shapeId?.includes("Cat")) return mockShapeObject2 as unknown as T;
console.warn(
"BACKEND: requestShape for unknown shape, returning empty object.",
shapeId
);
return {} as T;
}
// Register handler for messages coming from js-land
communicationChannel.addEventListener(
"message",
(event: MessageEvent<WasmMessage>) => {
console.log("BACKEND: Received message", event.data);
// call WASM ng-sdk-js
const { type, connectionId, shapeType } = event.data;
if (type === "Request") {
ng.orm_update("", "", {}, 1);
/* unsub = await ng.orm_start(scope, shapeType, session_id,
async (response) => {
//console.log("GOT APP RESPONSE", response);
if (response.V0.OrmInitial) {
} else if (response.V0.OrmUpdate) {
let diff = response.V0.OrmUpdate.diff
const msg: WasmMessage = {
type: "BackendUpdate",
connectionId,
diff,
};
communicationChannel.postMessage(msg);
} else if (response.V0.OrmError) {
}
*/
const shapeId = shapeType?.shape;
const initialData = getInitialObjectByShapeId(shapeId);
// Store connection. We store the shapeId string to allow equality across connections.
shapeManager.connections.set(connectionId, {
id: connectionId,
// Cast to any to satisfy WasmConnection type, comparison in updateShape uses ==
shape: (shapeId ?? "__unknown__") as any,
state: initialData,
callback: (diff: Diff, conId: WasmConnection["id"]) => {
// Notify js-land about backend updates
const msg: WasmMessage = {
type: "BackendUpdate",
connectionId,
diff,
};
communicationChannel.postMessage(msg);
},
});
const msg: WasmMessage = {
type: "InitialResponse",
connectionId,
initialData,
};
communicationChannel.postMessage(msg);
return;
}
if (type === "Stop") {
shapeManager.connections.delete(connectionId);
// await ng.app_request ( OrmStop )
return;
}
if (type === "FrontendUpdate" && event.data.diff) {
updateShape(connectionId, event.data.diff);
return;
}
console.warn(
"BACKEND: Unknown message type or missing diff",
event.data
);
}
);
ng.orm_start("", TestObjectShapeType, 12, (...params: any) => {
console.log("updates received with params", params);
}).catch((err) => console.error(err));

@ -1,10 +0,0 @@
import type { Diff, ObjectState, WasmConnection } from "./types";
const connections: Map<WasmConnection["id"], WasmConnection> = new Map();
/** Mock function to apply diffs. Just uses a copy of the diff as the new object. */
export function applyDiff(currentState: ObjectState, diff: Diff): ObjectState {
return JSON.parse(JSON.stringify(diff));
}
export { connections };

@ -1,220 +0,0 @@
import type { Predicate, Shape, Schema } from "@nextgraph-monorepo/ng-shex-orm";
/**
* Build a SPARQL CONSTRUCT query from a ShapeConstraint definition.
* The WHERE mirrors the graph template. Optional predicates (min=0) are wrapped in OPTIONAL in WHERE
* but still appear in the CONSTRUCT template so that matched triples are constructed.
*/
export function buildConstructQuery(
shape: Shape,
schema: Schema,
options?: SparqlBuildOptions
): string {
const ctx: BuildContext = { usedVars: new Set<string>() };
const prefixes = prefixesToText(options?.prefixes);
const subject = shape.iri;
const templateLines: string[] = [];
const whereLines: string[] = [];
const postFilters: string[] = [];
const valuesBlocks: string[] = [];
const rootVar =
subject.startsWith("?") || subject.startsWith("$")
? subject
: uniqueVar(ctx, "s");
if (!subject.startsWith("?") && !subject.startsWith("$")) {
valuesBlocks.push(valuesBlock(rootVar, [subject] as any));
}
const predicates = Array.isArray(shape.predicates)
? shape.predicates
: [...shape.predicates];
for (const pred of predicates) {
addConstructPattern(
ctx,
pred,
rootVar,
templateLines,
whereLines,
postFilters,
valuesBlocks,
options
);
}
const graphWrap = (body: string) =>
options?.graph
? `GRAPH ${toIriOrCurie(options.graph)} {\n${body}\n}`
: body;
const where = [
...valuesBlocks,
graphWrap(whereLines.join("\n")),
...postFilters,
]
.filter(Boolean)
.join("\n");
const template = templateLines.join("\n");
return [prefixes, `CONSTRUCT {`, template, `} WHERE {`, where, `}`].join(
"\n"
);
}
function addConstructPattern(
ctx: BuildContext,
pred: Predicate,
subjectVar: string,
template: string[],
where: string[],
postFilters: string[],
valuesBlocks: string[],
options?: SparqlBuildOptions
) {
const p = `<${pred.iri}>`;
const objVar = uniqueVar(ctx, pred.readablePredicate);
const triple = `${subjectVar} ${p} ${objTerm} .`;
const isOptional =
(pred.minCardinality ?? 0) === 0 &&
(options?.includeOptionalForMinZero ?? true);
if (pred.dataTypes === "nested" && pred.nestedShape) {
template.push(triple);
const nestedBody: string[] = [triple];
const nestedPreds = pred.nestedShape.predicates;
for (const n of nestedPreds) {
addConstructPattern(
ctx,
n,
objTerm,
template,
nestedBody,
postFilters,
valuesBlocks,
options
);
}
const block = nestedBody.join("\n");
where.push(isOptional ? `OPTIONAL {\n${block}\n}` : block);
return;
}
// Non-nested
template.push(triple);
const blockLines: string[] = [triple];
if (pred.dataTypes === "literal" && pred.literalValue !== undefined) {
if (Array.isArray(pred.literalValue)) {
valuesBlocks.push(valuesBlock(objVar, pred.literalValue as any[]));
} else {
const lit =
typeof pred.literalValue === "string" ||
typeof pred.literalValue === "number" ||
typeof pred.literalValue === "boolean"
? pred.literalValue
: String(pred.literalValue);
postFilters.push(
`FILTER(${objVar} = ${typeof lit === "string" ? `"${String(lit).replace(/"/g, '\\"')}"` : lit})`
);
}
}
const block = blockLines.join("\n");
where.push(isOptional ? `OPTIONAL {\n${block}\n}` : block);
}
export type LiteralKind =
| "number"
| "string"
| "boolean"
| "nested"
| "literal";
export interface SparqlBuildOptions {
prefixes?: Record<string, string>;
graph?: string; // IRI of the named graph to query, if any
includeOptionalForMinZero?: boolean; // default true
}
export const defaultPrefixes: Record<string, string> = {
xsd: "http://www.w3.org/2001/XMLSchema#",
rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
};
export function prefixesToText(prefixes?: Record<string, string>): string {
const all = { ...defaultPrefixes, ...(prefixes ?? {}) };
return Object.entries(all)
.map(([p, iri]) => `PREFIX ${p}: <${iri}>`)
.join("\n");
}
export function safeVarName(name: string): string {
const base = name
.replace(/[^a-zA-Z0-9_]/g, "_")
.replace(/^([0-9])/, "_$1")
.slice(0, 60);
return base || "v";
}
export function varToken(name: string): string {
const n =
name.startsWith("?") || name.startsWith("$") ? name.slice(1) : name;
return `?${safeVarName(n)}`;
}
export function formatLiteral(value: string | number | boolean): string {
if (typeof value === "number") return String(value);
if (typeof value === "boolean") return value ? "true" : "false";
// default string literal
const escaped = value.replace(/"/g, '\\"');
return `"${escaped}"`;
}
export function formatTermForValues(value: string | number | boolean): string {
if (typeof value === "number" || typeof value === "boolean")
return formatLiteral(value);
// strings: detect IRI or CURIE and keep raw; otherwise quote
const v = value.trim();
const looksLikeIri = v.startsWith("<") && v.endsWith(">");
const looksLikeHttp = v.includes("://");
const looksLikeCurie =
/^[A-Za-z_][A-Za-z0-9_-]*:.+$/u.test(v) && !looksLikeHttp;
if (looksLikeIri || looksLikeHttp || looksLikeCurie) {
return looksLikeHttp ? `<${v}>` : v;
}
return formatLiteral(v);
}
export function valuesBlock(
varName: string,
values: Array<string | number | boolean>
): string {
const rendered = values.map(formatTermForValues).join(" ");
return `VALUES ${varName} { ${rendered} }`;
}
export interface BuildContext {
// Tracks used variable names to avoid collisions
usedVars: Set<string>;
}
export function uniqueVar(ctx: BuildContext, base: string): string {
let candidate = varToken(base);
if (!ctx.usedVars.has(candidate)) {
ctx.usedVars.add(candidate);
return candidate;
}
let i = 2;
while (ctx.usedVars.has(`${candidate}_${i}`)) i++;
const unique = `${candidate}_${i}`;
ctx.usedVars.add(unique);
return unique;
}
export default buildConstructQuery;

@ -1,140 +0,0 @@
import type { Predicate, Shape, Schema } from "@nextgraph-monorepo/ng-shex-orm";
export const buildConstructQuery = ({
schema,
shapeId,
}: {
schema: Schema;
shapeId: keyof Schema;
}): string => {
const rootShape = schema[shapeId];
const constructStatements: {
s: string;
p: string;
o: string;
optional: boolean;
literals: Predicate["literalValue"];
}[] = [];
const idToVarName: Record<string, string> = {};
const getVarNameFor = (id: string) => {
const currentName = idToVarName[id];
if (currentName) return currentName;
const newVar = `o${Object.entries(idToVarName).length + 1}`;
idToVarName[id] = newVar;
return newVar;
};
// Create s,p,o records where subject and object var names are mapped to shape or predicate ids.
const addTriples = (shape: Shape) => {
const predicates = shape.predicates;
const shapeId = shape.iri;
for (const pred of predicates) {
const subjectVarName = getVarNameFor(shapeId);
if (pred.dataTypes === "nested") {
if (typeof pred.nestedShape !== "string")
throw new Error("Nested shapes must be by reference");
// If a name for this shape was assigned already, it's triples have been added
// and we don't have to recurse.
const shapeAlreadyRegistered = !!idToVarName[pred.nestedShape];
const shapeVarName = getVarNameFor(pred.nestedShape);
constructStatements.push({
s: `?${subjectVarName}`,
p: `<${pred.iri}>`,
o: `?${shapeVarName}`,
optional: pred.minCardinality < 1,
literals: pred.literalValue,
// TODO: eitherOf ?
});
if (!shapeAlreadyRegistered)
addTriples(schema[pred.nestedShape]);
} else {
const objVarName = getVarNameFor(
shapeId + "__separator__" + pred.iri
);
constructStatements.push({
s: `?${subjectVarName}`,
p: `<${pred.iri}>`,
o: `?${objVarName}`,
optional: pred.minCardinality < 1,
literals: pred.literalValue,
// TODO: eitherOf ?
});
}
}
};
addTriples(rootShape);
const construct = `CONSTRUCT {
${constructStatements.map(({ s, p, o }) => ` ${s} ${p} ${o} .\n`).join("")} }`;
const statementToWhere = ({
s,
p,
o,
optional,
}: {
s: string;
p: string;
o: string;
optional: boolean;
}) => {
if (optional) return ` OPTIONAL { ${s} ${p} ${o} . }\n`;
else return ` ${s} ${p} ${o} .\n`;
};
const literalToSparqlFormat = (
literal: string | number | boolean
): string => {
if (typeof literal === "number") return String(literal);
if (typeof literal === "boolean") return literal ? "true" : "false";
if (typeof literal === "string") {
return isIri(literal)
? `<${literal}>`
: `"${escapeString(literal)}"`;
}
return `"${String(literal)}"`;
};
// Filters for optional values.
const filters = constructStatements
.filter((statement) => statement.literals !== undefined)
.map((statement) => {
const vals = arrayOf(statement.literals!);
if (vals.length === 0) return "";
if (vals.length === 1) {
return ` FILTER(${statement.o} = ${literalToSparqlFormat(vals[0]!)})\n`;
}
const list = vals.map(literalToSparqlFormat).join(", ");
return ` FILTER(${statement.o} IN (${list}))\n`;
})
.join("");
const where = `WHERE {
${constructStatements.map(statementToWhere).join("")}
${filters}
}`;
return `${construct}\n${where}`;
};
const arrayOf = <T extends any>(arrayOrLiteral: T | T[]) => {
if (typeof arrayOrLiteral === "undefined" || arrayOrLiteral === null)
return [];
if (Array.isArray(arrayOrLiteral)) return arrayOrLiteral;
return [arrayOrLiteral];
};
const isIri = (str: string) => /^[a-zA-Z][a-zA-Z0-9+.-]{1,7}:/.test(str);
const escapeString = (str: string) => str.replace(/["\\]/g, "\\$&");

@ -1,9 +0,0 @@
import { buildConstructQuery } from "./buildSparqlConstructFromShape.ts";
import { testShapeSchema } from "./testShape.schema.ts";
console.log(
buildConstructQuery({
schema: testShapeSchema,
shapeId: "http://example.org/TestObject",
})
);

@ -1,129 +0,0 @@
import type { Schema } from "@nextgraph-monorepo/ng-shex-orm";
/**
* =============================================================================
* testShapeSchema: Schema for testShape
* =============================================================================
*/
export const testShapeSchema: Schema = {
"http://example.org/TestObject": {
iri: "http://example.org/TestObject",
predicates: [
{
dataTypes: "literal",
literalValue: ["TestObject"],
maxCardinality: 1,
minCardinality: 1,
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
readablePredicate: "type",
extra: true,
},
{
dataTypes: "string",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/stringValue",
readablePredicate: "stringValue",
},
{
dataTypes: "number",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/numValue",
readablePredicate: "numValue",
},
{
dataTypes: "boolean",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/boolValue",
readablePredicate: "boolValue",
},
{
dataTypes: "number",
maxCardinality: -1,
minCardinality: 0,
iri: "http://example.org/arrayValue",
readablePredicate: "arrayValue",
},
{
dataTypes: "nested",
nestedShape:
"http://example.org/TestObject||http://example.org/objectValue",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/objectValue",
readablePredicate: "objectValue",
},
{
dataTypes: "nested",
nestedShape:
"http://example.org/TestObject||http://example.org/anotherObject",
maxCardinality: -1,
minCardinality: 0,
iri: "http://example.org/anotherObject",
readablePredicate: "anotherObject",
},
{
dataTypes: "eitherOf",
eitherOf: [
{
valType: "string",
},
{
valType: "number",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/numOrStr",
readablePredicate: "numOrStr",
},
],
},
"http://example.org/TestObject||http://example.org/objectValue": {
iri: "http://example.org/TestObject||http://example.org/objectValue",
predicates: [
{
dataTypes: "string",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/nestedString",
readablePredicate: "nestedString",
},
{
dataTypes: "number",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/nestedNum",
readablePredicate: "nestedNum",
},
{
dataTypes: "number",
maxCardinality: -1,
minCardinality: 0,
iri: "http://example.org/nestedArray",
readablePredicate: "nestedArray",
},
],
},
"http://example.org/TestObject||http://example.org/anotherObject": {
iri: "http://example.org/TestObject||http://example.org/anotherObject",
predicates: [
{
dataTypes: "string",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/prop1",
readablePredicate: "prop1",
},
{
dataTypes: "number",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/prop2",
readablePredicate: "prop2",
},
],
},
};

@ -1,18 +0,0 @@
import type { ShapeType, BaseType } from "@nextgraph-monorepo/ng-shex-orm";
import type { Patch } from "@nextgraph-monorepo/ng-signals";
/** The Scope of a shape request */
export type Scope = string | string[];
/** The diff format used to communicate updates between wasm-land and js-land. */
export type Diff = Patch[];
export type ObjectState = object;
/** A connection established between wasm-land and js-land for subscription of a shape. */
export type WasmConnection<T extends BaseType = BaseType> = {
id: string;
shape: ShapeType<T>;
state: ObjectState;
callback: (diff: Diff, connectionId: WasmConnection["id"]) => void;
};

@ -1,22 +0,0 @@
import * as shapeManager from "./shapeManager";
import type { WasmConnection, Diff } from "./types";
export default async function updateShape(
connectionId: WasmConnection["id"],
diff: Diff,
) {
const connection = shapeManager.connections.get(connectionId);
if (!connection) throw new Error("No Connection found.");
console.log("BACKEND: Received update request from ", connectionId);
const newState = shapeManager.applyDiff(connection.state, diff);
connection.state = newState;
shapeManager.connections.forEach((con) => {
// if (con.shape == connection.shape) {
// con.state = newState;
// con.callback(diff, con.id);
// }
});
}

@ -0,0 +1,235 @@
import { describe, test, expect } from "vitest";
import { applyDiff, Patch } from "../index.ts";
/**
* Build a patch path string from segments (auto-prefix /)
*/
function p(...segs: (string | number)[]) {
return "/" + segs.map(String).join("/");
}
describe("applyDiff - set operations (primitives)", () => {
test("add single primitive into new set", () => {
const state: any = {};
const diff: Patch[] = [
{ op: "add", valType: "set", path: p("tags"), value: "a" },
];
applyDiff(state, diff);
expect(state.tags).toBeInstanceOf(Set);
expect([...state.tags]).toEqual(["a"]);
});
test("add multiple primitives into new set", () => {
const state: any = {};
const diff: Patch[] = [
{ op: "add", valType: "set", path: p("nums"), value: [1, 2, 3] },
];
applyDiff(state, diff);
expect([...state.nums]).toEqual([1, 2, 3]);
});
test("add primitives merging into existing set", () => {
const state: any = { nums: new Set([1]) };
const diff: Patch[] = [
{ op: "add", valType: "set", path: p("nums"), value: [2, 3] },
];
applyDiff(state, diff);
expect([...state.nums].sort()).toEqual([1, 2, 3]);
});
test("remove single primitive from set", () => {
const state: any = { tags: new Set(["a", "b"]) };
const diff: Patch[] = [
{ op: "remove", valType: "set", path: p("tags"), value: "a" },
];
applyDiff(state, diff);
expect([...state.tags]).toEqual(["b"]);
});
test("remove multiple primitives from set", () => {
const state: any = { nums: new Set([1, 2, 3, 4]) };
const diff: Patch[] = [
{ op: "remove", valType: "set", path: p("nums"), value: [2, 4] },
];
applyDiff(state, diff);
expect([...state.nums].sort()).toEqual([1, 3]);
});
});
describe("applyDiff - set operations (object sets)", () => {
test("add object entries to new object-set", () => {
const state: any = {};
const diff: Patch[] = [
{
op: "add",
valType: "set",
path: p("users"),
value: { u1: { id: "u1", n: 1 }, u2: { id: "u2", n: 2 } },
},
];
applyDiff(state, diff);
expect(state.users.u1).toEqual({ id: "u1", n: 1 });
expect(state.users.u2).toEqual({ id: "u2", n: 2 });
});
test("merge object entries into existing object-set", () => {
const state: any = { users: { u1: { id: "u1", n: 1 } } };
const diff: Patch[] = [
{
op: "add",
valType: "set",
path: p("users"),
value: { u2: { id: "u2", n: 2 } },
},
];
applyDiff(state, diff);
expect(Object.keys(state.users).sort()).toEqual(["u1", "u2"]);
});
test("remove object entries from object-set", () => {
const state: any = { users: { u1: {}, u2: {}, u3: {} } };
const diff: Patch[] = [
{
op: "remove",
valType: "set",
path: p("users"),
value: ["u1", "u3"],
},
];
applyDiff(state, diff);
expect(Object.keys(state.users)).toEqual(["u2"]);
});
test("adding primitives to existing object-set replaces with Set", () => {
const state: any = { mixed: { a: {}, b: {} } };
const diff: Patch[] = [
{ op: "add", valType: "set", path: p("mixed"), value: [1, 2] },
];
applyDiff(state, diff);
expect(state.mixed).toBeInstanceOf(Set);
expect([...state.mixed]).toEqual([1, 2]);
});
});
describe("applyDiff - object & literal operations", () => {
test("add object (create empty object)", () => {
const state: any = {};
const diff: Patch[] = [
{ op: "add", path: p("address"), valType: "object" },
];
applyDiff(state, diff);
expect(state.address).toEqual({});
});
test("add nested object path with ensurePathExists", () => {
const state: any = {};
const diff: Patch[] = [
{ op: "add", path: p("a", "b", "c"), valType: "object" },
];
applyDiff(state, diff, true);
expect(state.a.b.c).toEqual({});
});
test("add primitive value", () => {
const state: any = { address: {} };
const diff: Patch[] = [
{ op: "add", path: p("address", "street"), value: "1st" },
];
applyDiff(state, diff);
expect(state.address.street).toBe("1st");
});
test("overwrite primitive value", () => {
const state: any = { address: { street: "old" } };
const diff: Patch[] = [
{ op: "add", path: p("address", "street"), value: "new" },
];
applyDiff(state, diff);
expect(state.address.street).toBe("new");
});
test("remove primitive", () => {
const state: any = { address: { street: "1st", country: "Greece" } };
const diff: Patch[] = [{ op: "remove", path: p("address", "street") }];
applyDiff(state, diff);
expect(state.address.street).toBeUndefined();
expect(state.address.country).toBe("Greece");
});
test("remove object branch", () => {
const state: any = { address: { street: "1st" }, other: 1 };
const diff: Patch[] = [{ op: "remove", path: p("address") }];
applyDiff(state, diff);
expect(state.address).toBeUndefined();
expect(state.other).toBe(1);
});
});
describe("applyDiff - multiple mixed patches in a single diff", () => {
test("sequence of mixed set/object/literal add & remove", () => {
const state: any = {
users: { u1: { id: "u1" } },
tags: new Set(["old"]),
};
const diff: Patch[] = [
{
op: "add",
valType: "set",
path: p("users"),
value: { u2: { id: "u2" } },
},
{ op: "add", path: p("profile"), valType: "object" },
{ op: "add", path: p("profile", "name"), value: "Alice" },
{ op: "add", valType: "set", path: p("tags"), value: ["new"] },
{ op: "remove", valType: "set", path: p("tags"), value: "old" },
];
applyDiff(state, diff);
expect(Object.keys(state.users).sort()).toEqual(["u1", "u2"]);
expect(state.profile.name).toBe("Alice");
expect([...state.tags]).toEqual(["new"]);
});
test("complex nested path creation and mutations with ensurePathExists", () => {
const state: any = {};
const diff: Patch[] = [
{ op: "add", path: p("a", "b"), valType: "object" },
{ op: "add", path: p("a", "b", "c"), value: 1 },
{
op: "add",
valType: "set",
path: p("a", "nums"),
value: [1, 2, 3],
},
{ op: "remove", valType: "set", path: p("a", "nums"), value: 2 },
{ op: "add", path: p("a", "b", "d"), value: 2 },
{ op: "remove", path: p("a", "b", "c") },
];
applyDiff(state, diff, true);
expect(state.a.b.c).toBeUndefined();
expect(state.a.b.d).toBe(2);
expect(state.a.nums).toBeInstanceOf(Set);
expect([...state.a.nums].sort()).toEqual([1, 3]);
});
});
describe("applyDiff - ignored / invalid scenarios", () => {
test("skip patch with non-leading slash path", () => {
const state: any = {};
const diff: Patch[] = [
{ op: "add", path: "address/street", value: "x" },
];
applyDiff(state, diff);
expect(state).toEqual({});
});
test("missing parent without ensurePathExists -> patch skipped and no mutation", () => {
const state: any = {};
const diff: Patch[] = [{ op: "add", path: p("a", "b", "c"), value: 1 }];
applyDiff(state, diff, false);
expect(state).toEqual({});
});
});
describe("applyDiff - ignored / invalid scenarios", () => {
test("skip patch with non-leading slash path", () => {
const state: any = {};
const diff: Patch[] = [
{ op: "add", path: "address/street", value: "x" },
];
applyDiff(state, diff);
expect(state).toEqual({});
});
test("missing parent without ensurePathExists -> patch skipped and no mutation", () => {
const state: any = {};
const diff: Patch[] = [{ op: "add", path: p("a", "b", "c"), value: 1 }];
applyDiff(state, diff, false);
expect(state).toEqual({});
});
});
Loading…
Cancel
Save