Implement scoped interface extents across QX, generated bindings, SDK, Orch, Camino and browser queries. Preserve native projection, aggregation, pagination and watch invalidation without synthetic persisted objects. Validate exact query grants and scope freshness on execution and replay. Add local PostgreSQL, transport, browser and code-generation coverage. C1 implementation slice only: full-text indexes and Studio search adoption (C2/C3) remain tracked in the implementation handoff. No publication or deployment.
299 lines
13 KiB
TypeScript
299 lines
13 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 { 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";
|
|
|
|
const source = { repository: "https://example.test/queries.git", commit: "a".repeat(40) };
|
|
const iface = (text: string, dependencies: InterfaceRevision[] = []) => {
|
|
const compiled = compileCapabilityResourceSource(text, {
|
|
source,
|
|
environment: {
|
|
interfaces: new Map(dependencies.map((entry) => [entry.displayName, entry])),
|
|
interfaceClosure: dependencies,
|
|
},
|
|
});
|
|
assert.ok(compiled.ok, JSON.stringify(compiled.diagnostics));
|
|
assert.equal(compiled.resource.kind, "interface");
|
|
if (compiled.resource.kind !== "interface") throw new Error("interface required");
|
|
return compiled.resource.revision;
|
|
};
|
|
const facts = iface(`interface TaskFacts id "facts" revision "facts@1" {
|
|
queryable value title id "title" : string { get id "title:get"; }
|
|
queryable value done id "done" : bool { get id "done:get"; }
|
|
queryable value due id "due" : optional<int64> { get id "due:get"; }
|
|
queryable rpc value score id "score" : int32 { get id "score:get"; }
|
|
value hidden id "hidden" : string { get id "hidden:get"; }
|
|
}`);
|
|
const collection = iface(
|
|
`import interface TaskFacts;
|
|
interface Tasks id "tasks" revision "tasks@1" {
|
|
queryable relation items id "items" : many interface TaskFacts ordered { resolve id "items:resolve"; }
|
|
}`,
|
|
[facts],
|
|
);
|
|
|
|
test("generic query templates check declared bounds before closed specialization", async () => {
|
|
const generic = iface(
|
|
`import interface TaskFacts;
|
|
interface Collection<object Item implements TaskFacts> id "generic-collection" revision "generic-collection@1" {
|
|
queryable relation items id "items" : many object Item { resolve id "items:resolve"; }
|
|
}`,
|
|
[facts],
|
|
);
|
|
const build = (bound: string, workspace = false) =>
|
|
compileCapabilityResourceSource(
|
|
`import interface Collection; import interface TaskFacts;
|
|
package GenericQueries id "generic-queries" revision "generic-queries@1" {
|
|
query Rows<object Item ${bound}> id "rows-template" root ${workspace ? "workspace " : ""}Collection<Item>
|
|
document "rows.graphql" operation "Rows" { view object Item as TaskFacts; max rows 30; }
|
|
query TaskRows id "task-rows" specialize Rows<interface TaskFacts>;
|
|
}`,
|
|
{
|
|
source,
|
|
environment: {
|
|
interfaces: new Map([
|
|
["Collection", generic],
|
|
["TaskFacts", facts],
|
|
]),
|
|
interfaceClosure: [generic, facts],
|
|
},
|
|
},
|
|
);
|
|
const result = build("implements TaskFacts");
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
if (result.resource.kind !== "package") throw new Error("package expected");
|
|
const pkg = result.resource.revision;
|
|
assert.equal(pkg.queries?.length, 1);
|
|
assert.equal(pkg.queryTemplates?.length, 1);
|
|
assert.equal(pkg.queries![0]!.application!.templateId, "rows-template");
|
|
const read = async () => `query Rows { root { items(first: 3) { entries { node { title } } } } }`;
|
|
const universal = await checkQueryTemplate(pkg.queryTemplates![0]!, [generic, facts], read);
|
|
assert.equal(
|
|
universal.effects.some((effect) => effect.memberId === "title"),
|
|
true,
|
|
);
|
|
const specialized = await compileQuery(
|
|
pkg.queries![0]!,
|
|
[generic, facts, ...(result.resource.specializations ?? [])],
|
|
read,
|
|
);
|
|
assert.equal(
|
|
specialized.effects.some((effect) => effect.memberId === "title"),
|
|
true,
|
|
);
|
|
const invalid = build("");
|
|
assert.ok(invalid.ok, JSON.stringify(invalid.diagnostics));
|
|
if (invalid.resource.kind !== "package") throw new Error("package expected");
|
|
await assert.rejects(
|
|
checkQueryTemplate(invalid.resource.revision.queryTemplates![0]!, [generic, facts], read),
|
|
/bound|guarantee|implement/i,
|
|
);
|
|
const extent = build("implements TaskFacts", true);
|
|
assert.ok(extent.ok, JSON.stringify(extent.diagnostics));
|
|
if (extent.resource.kind !== "package") throw new Error("package expected");
|
|
const extentPackage = extent.resource.revision;
|
|
const readExtent = async () =>
|
|
`query Rows {root {objects(first: 2) {entries {node {items(first: 3) {entries {node {title}}}}}}}}`;
|
|
await checkQueryTemplate(extentPackage.queryTemplates![0]!, [generic, facts], readExtent);
|
|
const closed = await compileQuery(
|
|
extentPackage.queries![0]!,
|
|
[generic, facts, ...(extent.resource.specializations ?? [])],
|
|
readExtent,
|
|
);
|
|
assert.equal(closed.declaration.root.kind, "workspace-interface");
|
|
assert.equal(closed.declaration.root.interface, specialized.declaration.root.interface);
|
|
const invalidExtent = build("", true);
|
|
assert.ok(invalidExtent.ok, JSON.stringify(invalidExtent.diagnostics));
|
|
if (invalidExtent.resource.kind !== "package") throw new Error("package expected");
|
|
await assert.rejects(
|
|
checkQueryTemplate(invalidExtent.resource.revision.queryTemplates![0]!, [generic, facts], readExtent),
|
|
/bound|guarantee|implement/i,
|
|
);
|
|
});
|
|
function fixture(clauses = "", root = "Tasks") {
|
|
const result = compileCapabilityResourceSource(
|
|
`import interface Tasks; import interface TaskFacts;
|
|
package Queries id "queries" revision "queries@1" {
|
|
query Upcoming id "upcoming" root ${root} document "queries/upcoming.graphql" operation "Upcoming" {
|
|
fragments "queries/row.graphql"; max rows 30; ${clauses}
|
|
}
|
|
}`,
|
|
{
|
|
source,
|
|
environment: {
|
|
interfaces: new Map([
|
|
["Tasks", collection],
|
|
["TaskFacts", facts],
|
|
]),
|
|
interfaceClosure: [collection, facts],
|
|
},
|
|
},
|
|
);
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
assert.equal(result.resource.kind, "package");
|
|
if (result.resource.kind !== "package") throw new Error("package required");
|
|
assert.equal(result.resource.revision.exports.length, 0);
|
|
return result.resource.revision.queries![0]!;
|
|
}
|
|
test("workspace roots expose a bounded interface extent, not a root object or writable relationship", async () => {
|
|
const declaration = { ...fixture("watch;", "workspace TaskFacts"), fragments: [] };
|
|
assert.deepEqual(declaration.root, { kind: "workspace-interface", interface: "facts@1" });
|
|
const checked = await compileQuery(
|
|
declaration,
|
|
[facts, collection],
|
|
async () => `query Upcoming {
|
|
root {
|
|
objects(first: 3, where: {done: {eq: false}}, orderBy: {title: ASC}) {
|
|
entries {key node {_qx {ref} title @live}} pageInfo {hasNextPage endCursor}
|
|
}
|
|
_qx {relations {objects {aggregate {count sum {due}}}}}
|
|
}
|
|
}`,
|
|
);
|
|
assert.deepEqual(new Set(checked.effects.map((effect) => effect.memberId)), new Set(["done", "title", "due"]));
|
|
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}}}`),
|
|
/QUERY_VALIDATION/,
|
|
);
|
|
await assert.rejects(
|
|
compileQuery(declaration, [facts], async () => `query Upcoming {root {objects {entries {key}}}}`),
|
|
/QUERY_ROW_LIMIT/,
|
|
);
|
|
});
|
|
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 }
|
|
} }
|
|
}`;
|
|
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 () => {
|
|
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}`,
|
|
);
|
|
const fields = queryPresentation(checked, [collection, facts]);
|
|
assert.deepEqual(
|
|
fields.map((f) => f.path),
|
|
[
|
|
["root", "items", "entries", "*", "node", "label"],
|
|
["root", "items", "entries", "*", "node", "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);
|
|
});
|
|
|
|
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"],
|
|
])
|
|
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,
|
|
);
|
|
});
|
|
|
|
test("query dependency ports resolve exact exports without declaration ordering constraints", () => {
|
|
const result = compileCapabilityResourceSource(
|
|
`import interface Tasks;
|
|
package Queries id "queries" revision "queries@1" {
|
|
operation load id "read" : unit -> unit mode call receiver interfaces [Tasks]
|
|
requires { query upcoming id "upcoming-port" : Queries.Upcoming; };
|
|
query Upcoming id "upcoming" root Tasks document "upcoming.graphql" operation "Upcoming" { max rows 30; }
|
|
}`,
|
|
{ source, environment: { interfaces: new Map([["Tasks", collection]]), interfaceClosure: [collection, facts] } },
|
|
);
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
|
assert.equal(result.resource.kind, "package");
|
|
if (result.resource.kind !== "package") throw new Error("package expected");
|
|
assert.deepEqual(result.resource.revision.exports[0]!.dependencyPorts[0]!.requirement, {
|
|
kind: "query",
|
|
packageRevisionId: "queries@1",
|
|
queryId: "upcoming",
|
|
});
|
|
});
|
|
|
|
test("result types merge repeated selections and preserve conditional fragment fields", async () => {
|
|
const checked = await compile(
|
|
`query Upcoming($show: Boolean!) {
|
|
root { items(first: 3) { entries { node { title } } } }
|
|
root { items(first: 3) { entries { node { ...Row @include(if: $show) } } } }
|
|
}`,
|
|
"fragment Row on TaskFacts { due done }",
|
|
);
|
|
const output = JSON.stringify(checked.output);
|
|
assert.match(output, /\"title\":\{\"kind\":\"scalar\",\"name\":\"string\"\}/);
|
|
assert.match(output, /\"done\":\{\"kind\":\"optional\",\"value\":\{\"kind\":\"scalar\",\"name\":\"bool\"\}\}/);
|
|
assert.match(output, /\"due\"/);
|
|
});
|
|
|
|
test("fixed GraphQL yields exact effects, typed references and distinct query artifacts", async () => {
|
|
const checked = await compile();
|
|
assert.deepEqual(checked.variables, {
|
|
kind: "record",
|
|
fields: {
|
|
first: { kind: "scalar", name: "int32" },
|
|
before: { kind: "scalar", name: "int64" },
|
|
},
|
|
});
|
|
assert.match(JSON.stringify(checked.output), /"kind":"object-ref"/);
|
|
assert.ok(
|
|
checked.effects.some(
|
|
(effect) => effect.memberId === "due" && effect.uses.includes("order") && effect.uses.includes("predicate"),
|
|
),
|
|
);
|
|
assert.equal(checked.definitionDigest, (await compile()).definitionDigest);
|
|
assert.notEqual(
|
|
checked.definitionDigest,
|
|
(await compile(document, "fragment Row on TaskFacts { title }")).definitionDigest,
|
|
);
|
|
});
|
|
|
|
test("query allowlist and two-sided RPC effects fail during compilation", async () => {
|
|
const rejects = (promise: Promise<unknown>, code: string) =>
|
|
assert.rejects(promise, (error: unknown) => error instanceof QueryCompileError && error.code === code);
|
|
await rejects(compile(document, "fragment Row on TaskFacts { hidden }"), "QUERY_VALIDATION");
|
|
await rejects(compile(document, "fragment Row on TaskFacts { score }"), "QUERY_RPC_CONSUMER_REASON");
|
|
await compile(
|
|
document,
|
|
"fragment Row on TaskFacts { score }",
|
|
'allow TaskFacts.score select "Only the visible page is enriched";',
|
|
);
|
|
await rejects(
|
|
compile(document, "fragment Row on TaskFacts { score }", 'watch; allow TaskFacts.score select "Visible rows";'),
|
|
"QUERY_WATCH_UNSUPPORTED",
|
|
);
|
|
await compile(
|
|
document,
|
|
"fragment Row on TaskFacts { score }",
|
|
'watch; poll 5000 "External score has no push API"; allow TaskFacts.score select "Visible rows";',
|
|
);
|
|
await rejects(compile(document.replace("first: $first", "first: 31")), "QUERY_VALIDATION");
|
|
await rejects(compile(document.replace("...Row", "__typename ...Row")), "QUERY_UNSUPPORTED_FEATURE");
|
|
await rejects(compile(document, "fragment Row on TaskFacts { ...Row }"), "QUERY_VALIDATION");
|
|
});
|