166 lines
7.2 KiB
TypeScript
166 lines
7.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { compileCapabilityResourceSource } from "../src/capability-language/index.js";
|
|
import { compileQuery } from "../src/query/compile.js";
|
|
import type { InterfaceRevision } from "../src/capability-model/types.js";
|
|
|
|
const source = { repository: "https://example.test/tree", commit: "a".repeat(40) };
|
|
function iface(text: string, dependencies: InterfaceRevision[] = []) {
|
|
const result = compileCapabilityResourceSource(text, {
|
|
source,
|
|
environment: {
|
|
interfaces: new Map(dependencies.map((value) => [value.displayName, value])),
|
|
interfaceClosure: dependencies,
|
|
},
|
|
});
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
if (result.resource.kind !== "interface") throw Error("Expected Interface");
|
|
return result.resource.revision;
|
|
}
|
|
const item = iface(`interface Item id "item" revision "item@1" {
|
|
queryable value title id "title" : string {get id "title:get" execution snapshot;}
|
|
queryable value body id "body" : string {get id "body:get" execution snapshot;}
|
|
}`);
|
|
const navigation = iface(
|
|
`import interface Item; interface Navigation id "nav" revision "nav@1" {
|
|
queryable value chrome id "chrome" : string {get id "chrome:get" execution snapshot;}
|
|
queryable relation document id "document" : optional-one interface Item {resolve id "document:resolve" execution snapshot;}
|
|
queryable relation items id "items" : many interface Item ordered {resolve id "items:resolve" execution snapshot;}
|
|
}`,
|
|
[item],
|
|
);
|
|
function declaration(root: InterfaceRevision = item) {
|
|
const result = compileCapabilityResourceSource(
|
|
`import interface ${root.displayName}; package Views id "views" revision "views@1" {
|
|
query Open id "open" root ${root.displayName} document "open.graphql" operation "Open" {replica; max rows 10;}
|
|
}`,
|
|
{ source, environment: { interfaces: new Map([[root.displayName, root]]), interfaceClosure: [root, item] } },
|
|
);
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
if (result.resource.kind !== "package") throw Error("Expected Package");
|
|
return result.resource.revision.queries![0]!;
|
|
}
|
|
const fragments = `fragment Heading on Item {title} fragment Body on Item {body}`;
|
|
test("boundaries remain complete typed projections; ordinary spreads stay inline", async () => {
|
|
const checked = await compileQuery(
|
|
declaration(),
|
|
[item],
|
|
async () => `query Open {root {...Heading ...Body @boundary(name:"body")}} ${fragments}`,
|
|
);
|
|
assert(checked.tree);
|
|
assert(!JSON.stringify(checked.output).includes('"body"'));
|
|
assert(!JSON.stringify(checked.selection).includes('"body"'));
|
|
const body = checked.tree.nodes.find((node) => node.id === "root/boundary/body")!;
|
|
assert(body);
|
|
assert(
|
|
JSON.stringify(checked.tree.projections.find((projection) => projection.id === body.projection)!.output).includes(
|
|
'"body"',
|
|
),
|
|
);
|
|
assert.equal(checked.tree.nodes[0]!.boundaries[0]!.name, "body");
|
|
const ordinary = await compileQuery(
|
|
declaration(),
|
|
[item],
|
|
async () => `query Open {root {...Heading ...Body}} ${fragments}`,
|
|
);
|
|
assert.equal(ordinary.tree, undefined);
|
|
assert(JSON.stringify(ordinary.output).includes('"body"'));
|
|
});
|
|
test("minimal nullable-root bindings exclude slow siblings and carry child handles", async () => {
|
|
const checked = await compileQuery(
|
|
declaration(navigation),
|
|
[item, navigation],
|
|
async () => `query Open {root {
|
|
...Chrome @boundary(name:"chrome") document {...Heading @boundary(name:"basic") ...Body @boundary(name:"body")}
|
|
}} fragment Chrome on Navigation {chrome} ${fragments}`,
|
|
);
|
|
const tree = checked.tree!;
|
|
const binding = tree.nodes[0]!.bindings[0]!;
|
|
assert.equal(binding.name, "document");
|
|
const projection = tree.projections.find((projection) => projection.id === binding.projection)!;
|
|
assert(!JSON.stringify(projection.selection).includes('"chrome"'));
|
|
assert(!JSON.stringify(projection.selection).includes('"title"'));
|
|
assert(!JSON.stringify(projection.selection).includes('"body"'));
|
|
assert.deepEqual(binding.targets[0]!.path, ["document"]);
|
|
assert.deepEqual(
|
|
tree.nodes.find((node) => node.id === binding.targets[0]!.node)!.boundaries.map((boundary) => boundary.name),
|
|
["basic", "body"],
|
|
);
|
|
});
|
|
test("bounded list bindings preserve entry identity without requesting details", async () => {
|
|
const checked = await compileQuery(
|
|
declaration(navigation),
|
|
[item, navigation],
|
|
async () => `query Open {root {
|
|
items(first: 3) {entries {node {...Body @boundary(name:"body")}}}
|
|
}} fragment Body on Item {body}`,
|
|
);
|
|
const tree = checked.tree!,
|
|
binding = tree.nodes[0]!.bindings[0]!;
|
|
assert.deepEqual(binding.targets[0]!.path, ["items", "entries", "[]", "node"]);
|
|
const projection = tree.projections.find((projection) => projection.id === binding.projection)!;
|
|
assert(JSON.stringify(projection.selection).includes('"name":"key"'));
|
|
assert(!JSON.stringify(projection.selection).includes('"name":"body"'));
|
|
assert(tree.maxOccurrences <= 256);
|
|
});
|
|
test("guards control boundary participation and invalid syntax fails checking", async () => {
|
|
const checked = await compileQuery(
|
|
declaration(),
|
|
[item],
|
|
async () => `query Open($show:Boolean!) {root {
|
|
...Body @boundary(name:"body") @include(if:$show)
|
|
}} fragment Body on Item {body}`,
|
|
);
|
|
assert.deepEqual(checked.tree!.nodes[0]!.boundaries[0]!.conditions, [
|
|
{ include: true, value: { kind: "variable", name: "show" } },
|
|
]);
|
|
for (const [text, error] of [
|
|
[`query Open {root {...Body @boundary(name:"")}} fragment Body on Item {body}`, /QUERY_BOUNDARY_NAME/],
|
|
[
|
|
`query Open($name:String!) {root {...Body @boundary(name:$name)}} fragment Body on Item {body}`,
|
|
/QUERY_BOUNDARY_NAME/,
|
|
],
|
|
[
|
|
`query Open {root {...Body @boundary(name:"x") ...Heading @boundary(name:"x")}} ${fragments}`,
|
|
/QUERY_BOUNDARY_DUPLICATE/,
|
|
],
|
|
[`query Open {root {body @boundary(name:"x")}}`, /QUERY_VALIDATION/],
|
|
[`query Open {root {...Cycle @boundary(name:"x")}} fragment Cycle on Item {...Cycle}`, /QUERY_VALIDATION/],
|
|
] as const)
|
|
await assert.rejects(
|
|
compileQuery(declaration(), [item], async () => text),
|
|
error,
|
|
);
|
|
});
|
|
test("equivalent repeated paths share bindings; aliases preserve different windows", async () => {
|
|
const checked = await compileQuery(
|
|
declaration(navigation),
|
|
[item, navigation],
|
|
async () => `query Open {root {
|
|
document {...Heading @boundary(name:"basic")} document {...Body @boundary(name:"body")}
|
|
recent:items(first:2) {entries {node {...Body @boundary(name:"body")}}}
|
|
earlier:items(first:3) {entries {node {...Body @boundary(name:"body")}}}
|
|
}} ${fragments}`,
|
|
);
|
|
assert.deepEqual(
|
|
checked.tree!.nodes[0]!.bindings.map((binding) => binding.name),
|
|
["document", "recent", "earlier"],
|
|
);
|
|
assert.deepEqual(
|
|
checked
|
|
.tree!.nodes.find((node) => node.id === "root/binding/document")!
|
|
.boundaries.map((boundary) => boundary.name),
|
|
["basic", "body"],
|
|
);
|
|
await assert.rejects(
|
|
compileQuery(
|
|
declaration(navigation),
|
|
[item, navigation],
|
|
async () => `query Open($show:Boolean!) {root {
|
|
document @include(if:$show) {...Heading @boundary(name:"basic")} document {...Body @boundary(name:"body")}
|
|
}} ${fragments}`,
|
|
),
|
|
/QUERY_TREE_BINDING_CONFLICT/,
|
|
);
|
|
});
|