Standardize Camino query references and retire live field sidecars

This commit is contained in:
Timothy J. Aveni
2026-09-22 15:41:27 -07:00
parent 152f58a3c6
commit eba85ba158
28 changed files with 88 additions and 217 deletions
+14 -32
View File
@@ -4,7 +4,6 @@ import { compileCapabilityResourceSource } from "../src/capability-language/inde
import { compileQuery } from "../src/query/compile.js";
import { QueryCompileError } from "../src/query/types.js";
import { checkQueryTemplate } from "../src/query/templates.js";
import { queryPresentation } from "../src/query/presentation.js";
import { querySelectionToWire } from "../src/query/proto.js";
import type { InterfaceRevision } from "../src/capability-model/types.js";
@@ -149,9 +148,9 @@ test("workspace roots expose a bounded interface extent, not a root object or wr
async () => `query Upcoming {
root {
objects(first: 3, where: {done: {eq: false}}, orderBy: {title: ASC}) {
entries {key node {_qx {ref} title @live}} pageInfo {hasNextPage endCursor}
entries {key node {_camino {ref} title}} pageInfo {hasNextPage endCursor}
}
_qx {relations {objects {aggregate {count sum {due}}}}}
_camino {relations {objects {aggregate {count sum {due}}}}}
}
}`,
);
@@ -159,7 +158,7 @@ test("workspace roots expose a bounded interface extent, not a root object or wr
assert.ok(!checked.effects.some((effect) => effect.memberId.includes("workspace-objects")));
assert.ok(JSON.stringify(checked.selection).includes("qx-internal-workspace-objects"));
await assert.rejects(
compileQuery(declaration, [facts], async () => `query Upcoming {root {_qx {ref}}}`),
compileQuery(declaration, [facts], async () => `query Upcoming {root {_camino {ref}}}`),
/QUERY_VALIDATION/,
);
await assert.rejects(
@@ -169,51 +168,34 @@ test("workspace roots expose a bounded interface extent, not a root object or wr
});
const document = `query Upcoming($first: Int!, $before: Int64!) {
root { items(first: $first, where: {done: {eq: false}, due: {lt: $before}}, orderBy: [{due: ASC}]) {
entries { key node { _qx { ref } ...Row } } pageInfo { hasNextPage endCursor }
entries { key node { _camino { ref } ...Row } } pageInfo { hasNextPage endCursor }
} }
}`;
const compile = (query = document, row = "fragment Row on TaskFacts { title due }", clauses = "") =>
compileQuery(fixture(clauses), [collection, facts], async (name) => (name.endsWith("row.graphql") ? row : query));
test("live selections preserve aliases, nullable values, fragment conditions and wire identity", async () => {
test("ordinary selections preserve aliases, fragment conditions and wire identity", async () => {
const checked = await compile(
`query Upcoming($show: Boolean!) {root {items(first: 3) {entries {node {plain: title ...Row @include(if: $show)}}}}}`,
`fragment Row on TaskFacts {label: title @live due @live}`,
`fragment Row on TaskFacts {label: title due}`,
);
const fields = queryPresentation(checked, [collection, facts]);
const node = checked.selection[0]!.selection[0]!.selection[0]!.selection[0]!;
assert.deepEqual(
fields.map((f) => f.path),
[
["root", "items", "entries", "*", "node", "label"],
["root", "items", "entries", "*", "node", "due"],
],
node.selection.map((s) => s.key),
["plain", "label", "due"],
);
assert.equal(fields[0]!.getOperationId, "title:get");
assert.equal(fields[0]!.setOperationId, undefined);
assert.equal(fields[1]!.valueType.kind, "optional");
assert.equal(fields[0]!.conditions.length, 1);
assert.ok(JSON.stringify(checked.selection.map(querySelectionToWire)).includes(fields[0]!.selectionId));
const defaults = await compile(
`query Upcoming($show: Boolean! = true) {root {items(first: 3) {entries {node {...Row @include(if: $show)}}}}}`,
"fragment Row on TaskFacts {title @live}",
);
assert.equal(queryPresentation(defaults, [collection, facts])[0]!.conditions[0]!.defaultValue, true);
assert.equal(node.selection[1]!.conditions.length, 1);
assert.ok(JSON.stringify(checked.selection.map(querySelectionToWire)).includes("label"));
});
test("live selections reject RPC getters, synthetic selections and conflicting presentations", async () => {
for (const [selected, code] of [
["score @live", "QUERY_LIVE_UNSUPPORTED"],
["_qx @live {ref}", "QUERY_LIVE_UNSUPPORTED"],
["title @live title", "QUERY_LIVE_CONFLICT"],
["title @live(unchecked: true)", "QUERY_VALIDATION"],
])
test("retired live syntax fails with migration guidance", async () => {
for (const selected of ["score @live", "_camino @live {ref}", "title @live title", "title @live(unchecked: true)"])
await assert.rejects(
compile(
`query Upcoming {root {items(first: 3) {entries {node {...Row}}}}}`,
`fragment Row on TaskFacts {${selected}}`,
'allow TaskFacts.score select "bounded";',
),
(error: unknown) => error instanceof QueryCompileError && error.code === code,
(error: unknown) => error instanceof QueryCompileError && error.code === "QUERY_RETIRED_DIRECTIVE",
);
});