Support checked keyed relationships across portable operations and queries
This commit is contained in:
@@ -344,4 +344,11 @@ test("rule inverse reads retain the concrete conformance ownership checks", () =
|
||||
const missing = compile(source.replace("Membership.parent.resolve", "Missing.parent.resolve"));
|
||||
assert.equal(missing.ok, false);
|
||||
assert.match(JSON.stringify(missing.diagnostics), /Unknown attachment/);
|
||||
const unused = compile(
|
||||
source
|
||||
.replace('conform C as Child id "child-impl" {}', "")
|
||||
.replace("Membership.parent.resolve", "Missing.parent.resolve"),
|
||||
);
|
||||
assert.equal(unused.ok, false);
|
||||
assert.match(JSON.stringify(unused.diagnostics), /Unknown attachment/);
|
||||
});
|
||||
|
||||
@@ -129,6 +129,10 @@ await writeFile("dist/portable-reader/collections/kernel.json", JSON.stringify(c
|
||||
const { presentationMountPlan } = await import("./presentation-mount-fixture.js");
|
||||
const presentationMount = await presentationMountPlan();
|
||||
await mkdir("dist/portable-reader/presentation-mount", { recursive: true });
|
||||
await writeFile(
|
||||
"dist/portable-reader/presentation-mount/queries.json",
|
||||
JSON.stringify(compileQueryProgram(presentationMount)),
|
||||
);
|
||||
await writeFile("dist/portable-reader/presentation-mount/plan.json", JSON.stringify(presentationMount));
|
||||
await writeFile(
|
||||
"dist/portable-reader/presentation-mount/kernel.json",
|
||||
|
||||
+8
-1
@@ -1,3 +1,6 @@
|
||||
import interface PresentationStates;
|
||||
import interface ReaderNavigation;
|
||||
external atom ReaderNavigationRecord id "reader-navigation-record";
|
||||
import interface PresentationState;
|
||||
import interface PresentationMount;
|
||||
import interface StateFactory;
|
||||
@@ -12,7 +15,7 @@ package PresentationRuntime id "presentation-runtime" revision "presentation-run
|
||||
interface metadata id "state" : PresentationState using [presentationMount.resolve,parent.resolve] from input;
|
||||
};
|
||||
operation ensure<object Navigation implements PresentationState> id "presentation-runtime:ensure" : record {presentationMount:interface-ref<PresentationMount>;occurrence:string;parent:optional<interface-ref<PresentationState>>;} -> ref<Navigation> mode call execution transaction targets [browser,server] receiver any requires {
|
||||
edge states id "states" : many-unique object Navigation [resolve,connect];
|
||||
edge states id "states" : many-unique object Navigation keyed "string" [resolve,connect];
|
||||
interface metadata id "state" : PresentationState using [occurrence.get,presentationMount.resolve,parent.resolve] from input;
|
||||
interface presentationMount id "presentationMount" : PresentationMount using [attach.call] from input;
|
||||
interface factory id "factory" : StateFactory<Navigation> using [create.call];
|
||||
@@ -20,4 +23,8 @@ package PresentationRuntime id "presentation-runtime" revision "presentation-run
|
||||
function createState<object Navigation> id "presentation-runtime:create-state" : record {occurrence:string;parent:optional<interface-ref<PresentationState>>;} -> ref<Navigation> execution transaction targets [browser,server] requires {
|
||||
creation navigation id "navigation" : Navigation input record {occurrence:string;parent:optional<interface-ref<PresentationState>>;};
|
||||
};
|
||||
query ReaderStateInputs id "reader-state:inputs" root PresentationStates<atom ReaderNavigationRecord> document "queries/reader-inputs.graphql" operation "ReaderState" { replica; view ReaderNavigationRecord as ReaderNavigation; max rows 100; }
|
||||
query ReaderStateView id "reader-state:view" root PresentationStates<atom ReaderNavigationRecord> document "queries/reader-state.graphql" operation "ReaderState" { local from ReaderStateInputs; watch; view ReaderNavigationRecord as ReaderNavigation; max rows 100; }
|
||||
query ReaderWindowInputs id "reader-state:window-inputs" root PresentationStates<atom ReaderNavigationRecord> document "queries/reader-window.graphql" operation "ReaderState" { replica; view ReaderNavigationRecord as ReaderNavigation; max rows 100; }
|
||||
query ReaderWindowView id "reader-state:window-view" root PresentationStates<atom ReaderNavigationRecord> document "queries/reader-window.graphql" operation "ReaderState" { local from ReaderWindowInputs; watch; view ReaderNavigationRecord as ReaderNavigation; max rows 100; }
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import interface PresentationState;
|
||||
import interface PresentationMount;
|
||||
interface PresentationStates<object Navigation implements PresentationState> id "presentation-states" revision "presentation-states@1" {
|
||||
queryable relation states id "presentation-states:states" : many-unique object Navigation {
|
||||
queryable relation states id "presentation-states:states" : many-unique object Navigation keyed "string" {
|
||||
resolve id "presentation-states:states:resolve" execution snapshot targets [browser,server];
|
||||
}
|
||||
operation ensure id "presentation-states:ensure" : record {presentationMount:interface-ref<PresentationMount>;occurrence:string;parent:optional<interface-ref<PresentationState>>;} -> ref<Navigation> {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
query ReaderState {
|
||||
root {
|
||||
states(all: 100) {
|
||||
entries { key mapKey node { _camino { ref } page } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
query ReaderState($key: String!) {
|
||||
root {
|
||||
states(all: 100, where: {_camino: {mapKey: {eq: $key}}}) {
|
||||
entries { key mapKey node { _camino { ref } page } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
query ReaderState($key: String!) {
|
||||
root {
|
||||
states(first: 100, where: {_camino: {mapKey: {eq: $key}}}) {
|
||||
entries { key mapKey node { _camino { ref } page } }
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -10,6 +10,7 @@ workspace Example id "presentation-example" revision "presentation-example@1" co
|
||||
import package PresentationRuntime;
|
||||
atom PresentationMountRecord id "presentation-mount-record";
|
||||
atom Library id "library";
|
||||
atom OtherLibrary id "other-library";
|
||||
atom Document id "document";
|
||||
atom ReaderNavigationRecord id "reader-navigation-record";
|
||||
atom DocumentNavigationRecord id "document-navigation-record";
|
||||
@@ -34,8 +35,8 @@ workspace Example id "presentation-example" revision "presentation-example@1" co
|
||||
}
|
||||
conform<object Navigation implements PresentationState> Presentation<Navigation> as PresentationStates<Navigation> id "presentation:state-association" {
|
||||
private edge States id "presentation:states-edge" {
|
||||
atom Self projection states id "presentation:states-projection" many-unique;
|
||||
object Navigation projection subject id "presentation:subject-projection" exactly-one;
|
||||
atom Self projection states id "presentation:states-projection" many-unique keyed "string";
|
||||
object Navigation projection subject id "presentation:subject-projection" optional-one;
|
||||
}
|
||||
bind states.resolve to edge States.states.resolve;
|
||||
bind ensure.call to package PresentationRuntime.ensure<Navigation> with {
|
||||
@@ -78,4 +79,10 @@ workspace Example id "presentation-example" revision "presentation-example@1" co
|
||||
}
|
||||
conform Library as Presentation<atom ReaderNavigationRecord> id "library:presentation" {}
|
||||
conform Document as Presentation<atom DocumentNavigationRecord> id "document:presentation" {}
|
||||
conform OtherLibrary as StateFactory<atom ReaderNavigationRecord> id "other-reader:factory" {
|
||||
bind create.call to package PresentationRuntime.createState<atom ReaderNavigationRecord> with {
|
||||
navigation to creation ReaderNavigationRecord with {PresentationState from input;};
|
||||
};
|
||||
}
|
||||
conform OtherLibrary as Presentation<atom ReaderNavigationRecord> id "other-library:presentation" {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import {
|
||||
compileCapabilityResourceSource,
|
||||
compileCapabilitySource,
|
||||
formatQx,
|
||||
} from "../src/capability-language/index.js";
|
||||
import { compileExecutionPlan } from "../src/execution-world/plan.js";
|
||||
import { compileKernelProgram } from "../src/execution-world/kernel.js";
|
||||
import { generatePortableTypeScriptBindings } from "../src/bindings/portable.js";
|
||||
const source = { repository: "https://example.test/keyed.git", commit: "a".repeat(40) };
|
||||
const iface = compileCapabilityResourceSource(
|
||||
`interface Index<object Item> id "index" revision "index@1" {
|
||||
queryable relation entries id "entries" : many object Item keyed "string" {
|
||||
resolve id "resolve" execution snapshot targets [browser,server];
|
||||
connect id "connect" execution transaction targets [browser,server];
|
||||
}
|
||||
operation add id "add" : record {mapKey:string;target:ref<Item>;} -> unit {
|
||||
call id "add-call" execution transaction targets [browser,server];
|
||||
}
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(iface.ok && iface.resource.kind === "interface", JSON.stringify(iface.diagnostics));
|
||||
const pkg = compileCapabilityResourceSource(
|
||||
`package Runtime id "runtime" revision "runtime@1" {
|
||||
operation add<object Item> id "add" : record {mapKey:string;target:ref<Item>;} -> unit mode call execution transaction targets [browser,server] receiver any requires {
|
||||
edge entries id "entries" : many object Item keyed "string" [resolve,connect];
|
||||
};
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics));
|
||||
const indexContract = iface.resource.revision,
|
||||
runtimeContract = pkg.resource.revision;
|
||||
const declarations = `workspace W id "w" revision "w@1" commit "${source.commit}" {
|
||||
import interface Index; import package Runtime; atom Node id "node";
|
||||
conform Node as Index<atom Node> id "node:index" {
|
||||
private edge Entries id "entries" {
|
||||
atom Node projection entries id "entries" many keyed "string";
|
||||
atom Node projection parent id "parent" optional-one;
|
||||
}
|
||||
bind entries.resolve to edge Entries.entries.resolve;
|
||||
bind entries.connect to edge Entries.entries.connect;
|
||||
bind add.call to package Runtime.add<atom Node> with {entries to edge Entries.entries;};
|
||||
}
|
||||
}`;
|
||||
const compile = (text = declarations) =>
|
||||
compileCapabilitySource(formatQx(text), "workspace.qx", {
|
||||
interfaces: new Map([["Index", indexContract]]),
|
||||
packages: new Map([["Runtime", runtimeContract]]),
|
||||
});
|
||||
test("generic keyed relationships and portable ports have matching checked shapes", () => {
|
||||
const result = compile();
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
const closed = result.workspace.interfaceImports.find((i) => i.application)!;
|
||||
assert.equal(closed.members[0]!.kind, "relationship");
|
||||
assert.equal("keyType" in closed.members[0]! && closed.members[0]!.keyType, "string");
|
||||
const operation = closed.members[0]!.operations.find((o) => o.displayName === "connect")!;
|
||||
assert.equal(operation.inputType.kind, "record");
|
||||
const plan = compileExecutionPlan(result.workspace);
|
||||
assert.doesNotThrow(() => compileKernelProgram(plan));
|
||||
const bindings = generatePortableTypeScriptBindings(plan.definition.bindingSchema, "runtime@1", {
|
||||
target: "browser",
|
||||
});
|
||||
assert.match(bindings, /readonly "mapKey": string/);
|
||||
assert.match(bindings, /readonly "connect": \(input: \{ readonly "mapKey": string;/);
|
||||
});
|
||||
test("key contracts cannot silently bind to unkeyed storage or the inverse projection", () => {
|
||||
for (const text of [
|
||||
declarations.replace('many keyed "string";', "many;"),
|
||||
declarations.replace("entries to edge Entries.entries", "entries to edge Entries.parent"),
|
||||
]) {
|
||||
const result = compile(text);
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(JSON.stringify(result.diagnostics), /key type|keyed projection|signature/i);
|
||||
}
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
|
||||
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
|
||||
import { compileExecutionPlan } from "../src/execution-world/plan.js";
|
||||
import type { InterfaceRevision } from "../src/capability-model/types.js";
|
||||
import { compileQuery } from "../src/query/compile.js";
|
||||
export async function presentationMountPlan() {
|
||||
const read = (name: string) => readFile(`test/fixtures/presentation-mount/${name}.qx`, "utf8");
|
||||
const source = { repository: "https://example.test/presentation-mount.git", commit: "a".repeat(40) };
|
||||
@@ -31,5 +32,13 @@ export async function presentationMountPlan() {
|
||||
packages: new Map([["PresentationRuntime", pkg.resource.revision]]),
|
||||
});
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
const runtime = result.workspace.packageImports[0]!;
|
||||
runtime.checkedQueries = await Promise.all(
|
||||
runtime.queries!.map((query) =>
|
||||
compileQuery(query, result.workspace.interfaceImports, (path) =>
|
||||
readFile(`test/fixtures/presentation-mount/${path}`, "utf8"),
|
||||
),
|
||||
),
|
||||
);
|
||||
return compileExecutionPlan(result.workspace);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user