Add checked query composition foundations for prepared presentations
Move React navigation query generation behind an explicit build frontend. Add checked result-dependent query bindings and pure composition evaluation, including one-snapshot server execution. Preserve the existing rendering path while the React manifest, worker preparation, and replica admission integration remain pending; see docs/PREPARED_PRESENTATIONS_PROGRESS.md.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { compileCapabilityResourceSource } from "../src/capability-language/index.js";
|
||||
import type { InterfaceRevision } from "../src/capability-model/types.js";
|
||||
import { valueType } from "../src/capability-model/types.js";
|
||||
import { compileQuery } from "../src/query/compile.js";
|
||||
import { compileQueryComposition, type QueryComposition } from "../src/query/composition.js";
|
||||
import { defaultQueryBudgets } from "../src/query/types.js";
|
||||
|
||||
const iface = (text: string, dependencies: InterfaceRevision[] = []) => {
|
||||
const result = compileCapabilityResourceSource(text, {
|
||||
source: { repository: "https://example.test/devices", commit: "a".repeat(40) },
|
||||
environment: {
|
||||
interfaces: new Map(dependencies.map((entry) => [entry.displayName, entry])),
|
||||
interfaceClosure: dependencies,
|
||||
},
|
||||
});
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
if (result.resource.kind !== "interface") throw Error("Expected interface");
|
||||
return result.resource.revision;
|
||||
};
|
||||
|
||||
test("dependent query roots and arguments are checked without a presentation library", async () => {
|
||||
const sensor = iface(`interface Sensor id "sensor" revision "sensor@1" {
|
||||
queryable value reading id "reading" : int64 { get id "reading:get"; }
|
||||
}`);
|
||||
const control = iface(
|
||||
`import interface Sensor;
|
||||
interface Control id "control" revision "control@1" {
|
||||
queryable value enabled id "enabled" : bool { get id "enabled:get"; }
|
||||
queryable relation selected id "selected" : optional-one interface Sensor { resolve id "selected:get"; }
|
||||
}`,
|
||||
[sensor],
|
||||
);
|
||||
const query = (id: string, root: InterfaceRevision["revisionId"], text: string) =>
|
||||
compileQuery(
|
||||
{
|
||||
id,
|
||||
displayName: id,
|
||||
document: `${id}.graphql`,
|
||||
operation: id,
|
||||
root: { kind: "object", interface: root },
|
||||
fragments: [],
|
||||
views: [],
|
||||
allowances: [],
|
||||
budgets: { ...defaultQueryBudgets },
|
||||
watch: false,
|
||||
placement: { kind: "replica" },
|
||||
},
|
||||
[control, sensor],
|
||||
async () => text,
|
||||
);
|
||||
const selection = await query(
|
||||
"Selection",
|
||||
control.revisionId,
|
||||
"query Selection { root { enabled selected { _camino { ref atom } } } }",
|
||||
);
|
||||
const reading = await query(
|
||||
"Reading",
|
||||
sensor.revisionId,
|
||||
"query Reading($enabled: Boolean!) { root { reading @include(if: $enabled) } }",
|
||||
);
|
||||
const queries = new Map([
|
||||
["Selection", selection],
|
||||
["Reading", reading],
|
||||
]);
|
||||
const source: QueryComposition = {
|
||||
id: "device-display",
|
||||
input: { kind: "record", fields: { controller: valueType.interfaceRef(control.revisionId) } },
|
||||
maxQueries: 2,
|
||||
steps: [
|
||||
{
|
||||
name: "selection",
|
||||
query: "Selection",
|
||||
root: { from: "input", path: ["controller"] },
|
||||
arguments: { record: {} },
|
||||
},
|
||||
{
|
||||
name: "reading",
|
||||
query: "Reading",
|
||||
with: { from: "result", name: "selection", path: ["root", "selected"] },
|
||||
root: { from: "item", path: ["_camino", "ref"] },
|
||||
arguments: { record: { enabled: { from: "result", name: "selection", path: ["root", "enabled"] } } },
|
||||
},
|
||||
],
|
||||
};
|
||||
const checked = compileQueryComposition(source, queries);
|
||||
assert.equal(checked.output.kind, "record");
|
||||
assert.equal(checked.dependencies.length, 2);
|
||||
assert.ok(checked.definitionDigest);
|
||||
const changed = new Map(queries);
|
||||
changed.set("Reading", { ...reading, definitionDigest: "different-source" });
|
||||
assert.notEqual(compileQueryComposition(source, changed).definitionDigest, checked.definitionDigest);
|
||||
assert.throws(() => compileQueryComposition({ ...source, maxQueries: 1 }, queries), /exceed maxQueries/);
|
||||
assert.throws(
|
||||
() => compileQueryComposition({ ...source, steps: [...source.steps].reverse() }, queries),
|
||||
/earlier step/,
|
||||
);
|
||||
const wrongArguments = structuredClone(source);
|
||||
wrongArguments.steps[1]!.arguments = { record: { enabled: { from: "input", path: ["controller"] } } };
|
||||
assert.throws(() => compileQueryComposition(wrongArguments, queries), /arguments do not match/);
|
||||
const wrongRoot = structuredClone(source);
|
||||
wrongRoot.steps[1]!.root = { from: "input", path: ["controller"] };
|
||||
assert.throws(() => compileQueryComposition(wrongRoot, queries), /reference to sensor@1/);
|
||||
const nullableTraversal = structuredClone(source);
|
||||
nullableTraversal.steps[1]!.root = {
|
||||
from: "result",
|
||||
name: "selection",
|
||||
path: ["root", "selected", "_camino", "ref"],
|
||||
};
|
||||
assert.throws(() => compileQueryComposition(nullableTraversal, queries), /explicit boundary/);
|
||||
const branch = structuredClone(source);
|
||||
branch.steps[1]!.when = { equal: [{ from: "item", path: ["_camino", "atom"] }, { literal: "thermometer" }] };
|
||||
assert.ok(compileQueryComposition(branch, queries).definitionDigest);
|
||||
branch.steps[1]!.when = { literal: "not a boolean" };
|
||||
assert.throws(() => compileQueryComposition(branch, queries), /when requires a boolean/);
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import {
|
||||
compileCapabilityResourceRepository,
|
||||
type PackageQueryGenerator,
|
||||
} from "../src/capability-language/assembly.js";
|
||||
import { defaultQueryBudgets } from "../src/query/types.js";
|
||||
|
||||
test("an explicit non-React frontend emits checked queries for its own selection model", async (context) => {
|
||||
const directory = await mkdtemp(path.join(os.tmpdir(), "qx-query-generator-"));
|
||||
context.after(() => rm(directory, { recursive: true, force: true }));
|
||||
const commit = "a".repeat(40);
|
||||
const source = (name: string) => ({
|
||||
resolver: "git" as const,
|
||||
repository: `https://example.test/${name}.git`,
|
||||
commit,
|
||||
});
|
||||
const lock = (imports: string) => `quixos-lock version 1 {
|
||||
quixos source { repository "https://example.test/quixos.git"; commit "${commit}"; }
|
||||
${imports}
|
||||
}`;
|
||||
const dependency = (name: string) =>
|
||||
`interface ${name} source { repository "${source(name).repository}"; commit "${commit}"; }`;
|
||||
for (const name of ["Sensor", "DeviceSelection", "Device"]) await mkdir(path.join(directory, name));
|
||||
await writeFile(path.join(directory, "Sensor", "quixos.lock"), lock(""));
|
||||
await writeFile(
|
||||
path.join(directory, "Sensor", "interface.qx"),
|
||||
`
|
||||
interface Sensor id "sensor" revision "sensor@1" {
|
||||
queryable value reading id "reading" : int64 { get id "reading:get"; }
|
||||
}
|
||||
`,
|
||||
);
|
||||
await writeFile(path.join(directory, "DeviceSelection", "quixos.lock"), lock(dependency("Sensor")));
|
||||
await writeFile(
|
||||
path.join(directory, "DeviceSelection", "interface.qx"),
|
||||
`
|
||||
import interface Sensor;
|
||||
interface DeviceSelection id "device-selection" revision "device-selection@1" {
|
||||
queryable relation selectedSensor id "selected-sensor" : optional-one interface Sensor {
|
||||
resolve id "selected-sensor:resolve";
|
||||
}
|
||||
}
|
||||
`,
|
||||
);
|
||||
await writeFile(path.join(directory, "Device", "quixos.lock"), lock(dependency("DeviceSelection")));
|
||||
await writeFile(
|
||||
path.join(directory, "Device", "package.qx"),
|
||||
`
|
||||
import interface DeviceSelection;
|
||||
package Device id "device" revision "device@1" {}
|
||||
`,
|
||||
);
|
||||
await writeFile(
|
||||
path.join(directory, "Device", "inputs.graphql"),
|
||||
`query Selected { root { selectedSensor { reading } } }`,
|
||||
);
|
||||
const options = {
|
||||
rootDirectory: path.join(directory, "Device"),
|
||||
kind: "package" as const,
|
||||
source: source("Device"),
|
||||
resolveResource: async (ref: { repository: string }) => ({
|
||||
directory: path.join(directory, new URL(ref.repository).pathname.slice(1, -4)),
|
||||
}),
|
||||
};
|
||||
const plain = await compileCapabilityResourceRepository(options);
|
||||
assert.equal(plain.resource.kind, "package");
|
||||
if (plain.resource.kind !== "package") throw Error("Expected package");
|
||||
assert.equal(plain.resource.revision.queries?.length ?? 0, 0);
|
||||
|
||||
const generateQueries: PackageQueryGenerator = async ({ interfaces, readSource }) => ({
|
||||
queries: [
|
||||
{
|
||||
id: "selected-inputs",
|
||||
displayName: "SelectedInputs",
|
||||
root: {
|
||||
kind: "object",
|
||||
interface: interfaces.find((iface) => iface.displayName === "DeviceSelection")!.revisionId,
|
||||
},
|
||||
document: "generated/selected.graphql",
|
||||
operation: "Selected",
|
||||
fragments: [],
|
||||
views: [],
|
||||
allowances: [],
|
||||
budgets: { ...defaultQueryBudgets },
|
||||
watch: false,
|
||||
placement: { kind: "replica" },
|
||||
},
|
||||
],
|
||||
documents: new Map([["generated/selected.graphql", await readSource("inputs.graphql")]]),
|
||||
compositions: [
|
||||
{
|
||||
id: "display",
|
||||
maxQueries: 1,
|
||||
input: {
|
||||
kind: "record",
|
||||
fields: {
|
||||
device: {
|
||||
kind: "object-ref",
|
||||
expectation: {
|
||||
kind: "interface",
|
||||
interfaceRevisionId: interfaces.find((iface) => iface.displayName === "DeviceSelection")!.revisionId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
name: "selection",
|
||||
query: "selected-inputs",
|
||||
root: { from: "input", path: ["device"] },
|
||||
arguments: { record: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const built = await compileCapabilityResourceRepository({ ...options, generateQueries });
|
||||
assert.equal(built.resource.kind, "package");
|
||||
if (built.resource.kind !== "package") throw Error("Expected package");
|
||||
assert.equal(built.resource.revision.checkedQueries?.length, 1);
|
||||
const checked = built.resource.revision.checkedQueries![0]!;
|
||||
assert.equal(checked.declaration.root.interface, "device-selection@1");
|
||||
assert.ok(checked.effects.some((effect) => effect.memberId === "reading"));
|
||||
assert.ok(checked.effects.some((effect) => effect.memberId === "selected-sensor"));
|
||||
assert.ok(checked.definitionDigest);
|
||||
assert.equal(built.resource.revision.queryCompositions?.[0]?.dependencies[0]?.digest, checked.definitionDigest);
|
||||
assert.equal(
|
||||
built.resources.some((resource) => /React|PresentationMount/.test(resource.key)),
|
||||
false,
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
compileCapabilityResourceRepository({
|
||||
...options,
|
||||
generateQueries: async (input) => {
|
||||
const result = await generateQueries(input);
|
||||
return {
|
||||
...result,
|
||||
documents: new Map([["generated/selected.graphql", "query Selected { root { invented } }"]]),
|
||||
};
|
||||
},
|
||||
}),
|
||||
/invented/,
|
||||
);
|
||||
await assert.rejects(
|
||||
compileCapabilityResourceRepository({
|
||||
...options,
|
||||
generateQueries: async (input) => {
|
||||
const result = await generateQueries(input);
|
||||
return { ...result, queries: [...result.queries, ...result.queries] };
|
||||
},
|
||||
}),
|
||||
/Generated query conflicts/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user