Add exported query fragments and scoped coverage observations
This commit is contained in:
@@ -218,3 +218,50 @@ package Runtime id "package:runtime" revision "package:runtime@1" { }
|
||||
/requires external interface Named \(interface:named@1\)/,
|
||||
);
|
||||
});
|
||||
|
||||
test("resource-owned fragment imports compile through the locked package closure", async (context) => {
|
||||
const directory = await mkdtemp(path.join(os.tmpdir(), "quixos-fragment-assembly-"));
|
||||
context.after(() => rm(directory, { recursive: true, force: true }));
|
||||
const dirs = Object.fromEntries(["item", "rows", "screen"].map((name) => [name, path.join(directory, name)]));
|
||||
await Promise.all(Object.values(dirs).map((dir) => mkdir(dir)));
|
||||
const dependency = (kind: string, name: string) =>
|
||||
`${kind} ${name} source {repository "https://example.test/${name.toLowerCase()}.git"; commit "${namedCommit}";}`;
|
||||
await writeFile(path.join(dirs.item!, "quixos.lock"), lock());
|
||||
await writeFile(
|
||||
path.join(dirs.item!, "interface.qx"),
|
||||
`interface Item id "item" revision "item@1" {queryable value title id "title" : string {get id "title:get" execution snapshot targets [browser,server];}}`,
|
||||
);
|
||||
await writeFile(path.join(dirs.rows!, "quixos.lock"), lock(dependency("interface", "Item")));
|
||||
await writeFile(
|
||||
path.join(dirs.rows!, "package.qx"),
|
||||
`import interface Item; package Rows id "rows" revision "rows@1" {query fragment Row id "row" root Item document "row.graphql" fragment "Row";}`,
|
||||
);
|
||||
await writeFile(path.join(dirs.rows!, "row.graphql"), "fragment Row on Item {_camino {ref} title}");
|
||||
await writeFile(
|
||||
path.join(dirs.screen!, "quixos.lock"),
|
||||
lock(dependency("interface", "Item") + dependency("package", "Rows")),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(dirs.screen!, "package.qx"),
|
||||
`import interface Item; import package Rows; package Screen id "screen" revision "screen@1" {
|
||||
query Inputs id "inputs" root Item document "screen.graphql" operation "Screen" {replica; fragments Rows.Row;}
|
||||
query View id "view" root Item document "screen.graphql" operation "Screen" {local from Inputs; fragments Rows.Row;}
|
||||
}`,
|
||||
);
|
||||
await writeFile(path.join(dirs.screen!, "screen.graphql"), "query Screen {root {...Row}}");
|
||||
const result = await compileCapabilityResourceRepository({
|
||||
rootDirectory: dirs.screen!,
|
||||
kind: "package",
|
||||
source: { resolver: "git", repository: "https://example.test/screen.git", commit: packageCommit },
|
||||
resolveResource: async (source) => {
|
||||
const name = new URL(source.repository).pathname.slice(1, -4);
|
||||
assert.ok(dirs[name], name);
|
||||
return { directory: dirs[name]! };
|
||||
},
|
||||
});
|
||||
assert.equal(result.resource.kind, "package");
|
||||
if (result.resource.kind !== "package") throw Error("expected package");
|
||||
const view = result.resource.revision.checkedQueries!.find((q) => q.declaration.id === "view")!;
|
||||
assert.equal(view.selection[0]!.selection[0]!.fragmentSpread?.package, "rows@1");
|
||||
assert.equal(result.resources.length, 3);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { compileCapabilityResourceSource } from "../src/capability-language/parser.js";
|
||||
import { compileQuery } from "../src/query/compile.js";
|
||||
import type { PackageRevision } from "../src/capability-model/types.js";
|
||||
const source = { repository: "https://example.test/fragments", commit: "a".repeat(40) };
|
||||
const compiled = compileCapabilityResourceSource(
|
||||
`interface Item id "item" revision "item@1" {
|
||||
queryable value title id "title" : string {get id "title:get" execution snapshot targets [browser,server];}
|
||||
queryable rpc value remote id "remote" : string {get id "remote:get";}
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
if (!compiled.ok || compiled.resource.kind !== "interface") throw Error(JSON.stringify(compiled.diagnostics));
|
||||
const item = compiled.resource.revision;
|
||||
function pkg(body: string, imports: PackageRevision[] = [], name = "Views") {
|
||||
const result = compileCapabilityResourceSource(
|
||||
`import interface Item; ${imports.map((p) => `import package ${p.displayName};`).join(" ")} package ${name} id "${name}" revision "${name}@1" {${body}}`,
|
||||
{
|
||||
source,
|
||||
environment: {
|
||||
interfaces: new Map([["Item", item]]),
|
||||
interfaceClosure: [item],
|
||||
packages: new Map(imports.map((p) => [p.displayName, p])),
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
if (result.resource.kind !== "package") throw Error("package required");
|
||||
return result.resource.revision;
|
||||
}
|
||||
test("exported fragments generate local projection and direct admission", async () => {
|
||||
const p = pkg('query fragment Row id "row" root Item document "row.graphql" fragment "Row";');
|
||||
assert.equal(p.queries!.length, 2);
|
||||
const local = p.queries!.find((q) => !q.fragment?.admission)!;
|
||||
assert.equal(local.placement?.kind, "local");
|
||||
const checked = await compileQuery(local, [item], async () => "fragment Row on Item {_camino {ref} title}");
|
||||
assert.equal(checked.output.kind, "record");
|
||||
assert.match(checked.document, /query QuixosFragment/);
|
||||
await assert.rejects(
|
||||
compileQuery(local, [item], async () => "fragment Row on Item {remote}"),
|
||||
/QUERY_LOCAL_DEPENDENCY/,
|
||||
);
|
||||
});
|
||||
test("imported fragments retain checked occurrences and require exact roots", async () => {
|
||||
const provider = pkg('query fragment Row id "row" root Item document "row.graphql" fragment "Row";', [], "Provider");
|
||||
const checked = await compileQuery(
|
||||
provider.queries!.find((q) => !q.fragment?.admission)!,
|
||||
[item],
|
||||
async () => "fragment Row on Item {_camino {ref} title}",
|
||||
);
|
||||
const consumer = pkg(
|
||||
'query Screen id "screen" root Item document "screen.graphql" operation "Screen" {replica; fragments Provider.Row;}',
|
||||
[provider],
|
||||
);
|
||||
const result = await compileQuery(
|
||||
consumer.queries![0]!,
|
||||
[item],
|
||||
async () => "query Screen {root {...Row}}",
|
||||
async () => checked,
|
||||
);
|
||||
assert.equal(result.selection[0]!.selection[0]!.fragmentSpread?.digest, checked.definitionDigest);
|
||||
assert.equal(result.selection[0]!.selection[0]!.fragmentSpread?.root, item.revisionId);
|
||||
assert.ok(result.effects.some((e) => e.memberId === "title"));
|
||||
const wrong = {
|
||||
...checked,
|
||||
declaration: {
|
||||
...checked.declaration,
|
||||
root: { ...checked.declaration.root, interface: "different@1" as typeof item.revisionId },
|
||||
},
|
||||
};
|
||||
await assert.rejects(
|
||||
compileQuery(
|
||||
consumer.queries![0]!,
|
||||
[item],
|
||||
async () => "query Screen {root {...Row}}",
|
||||
async () => wrong,
|
||||
),
|
||||
/FRAGMENT_ROOT_MISMATCH/,
|
||||
);
|
||||
});
|
||||
test("fragment variable mappings are explicit and checked", async () => {
|
||||
const provider = pkg(
|
||||
'query fragment Row id "row" root Item document "row.graphql" fragment "Row" operation "Inputs";',
|
||||
[],
|
||||
"Provider",
|
||||
);
|
||||
const checked = await compileQuery(
|
||||
provider.queries!.find((q) => !q.fragment?.admission)!,
|
||||
[item],
|
||||
async () =>
|
||||
"query Inputs($show: Boolean!) {root {...Row}} fragment Row on Item {_camino {ref} title @include(if:$show)}",
|
||||
);
|
||||
const build = (mapping: string) =>
|
||||
pkg(
|
||||
`query Screen id "screen" root Item document "screen.graphql" operation "Screen" {replica; fragments Provider.Row ${mapping};}`,
|
||||
[provider],
|
||||
).queries![0]!;
|
||||
const document = "query Screen($visible: Boolean!) {root {...Row}}";
|
||||
await assert.rejects(
|
||||
compileQuery(
|
||||
build(""),
|
||||
[item],
|
||||
async () => document,
|
||||
async () => checked,
|
||||
),
|
||||
/FRAGMENT_VARIABLE_MAPPING/,
|
||||
);
|
||||
const result = await compileQuery(
|
||||
build("with {show to visible;}"),
|
||||
[item],
|
||||
async () => document,
|
||||
async () => checked,
|
||||
);
|
||||
assert.deepEqual(result.selection[0]!.selection[0]!.fragmentSpread?.variables, { show: "visible" });
|
||||
await assert.rejects(
|
||||
compileQuery(
|
||||
build("with {show to visible;}"),
|
||||
[item],
|
||||
async () => document.replace("Boolean!", "Boolean"),
|
||||
async () => checked,
|
||||
),
|
||||
/FRAGMENT_VARIABLE_TYPE/,
|
||||
);
|
||||
});
|
||||
|
||||
test("independent fragment package survives execution lowering and typed client generation", async () => {
|
||||
const { loadReaderWorkspace, readReaderFixture } = await import("./reader-fixture.js");
|
||||
const { compileExecutionPlan } = await import("../src/execution-world/plan.js");
|
||||
const { compileQueryProgram } = await import("../src/execution-world/queries.js");
|
||||
const { generatePackageQueryClientBindings } = await import("../src/execution-world/client-contracts.js");
|
||||
const workspace = await loadReaderWorkspace();
|
||||
const comment = workspace.interfaceImports.find((i) => i.displayName === "CommentData")!;
|
||||
const parsed = compileCapabilityResourceSource(
|
||||
`import interface CommentData; package CommentRows id "comment-rows" revision "comment-rows@1" {
|
||||
query fragment Row id "row" root CommentData document "row.graphql" fragment "CommentRow";
|
||||
}`,
|
||||
{ source, environment: { interfaces: new Map([["CommentData", comment]]), interfaceClosure: [comment] } },
|
||||
);
|
||||
assert.ok(parsed.ok, JSON.stringify(parsed.diagnostics));
|
||||
if (parsed.resource.kind !== "package") throw Error("expected package");
|
||||
const provider = parsed.resource.revision;
|
||||
provider.checkedQueries = await Promise.all(
|
||||
provider.queries!.map((q) =>
|
||||
compileQuery(
|
||||
q,
|
||||
workspace.interfaceImports,
|
||||
async () => "fragment CommentRow on CommentData {_camino {ref} content {text tags}}",
|
||||
),
|
||||
),
|
||||
);
|
||||
workspace.packageImports.push(provider);
|
||||
const consumer = workspace.packageImports.find((p) => p.displayName === "ReaderRuntime")!;
|
||||
const declaration = consumer.queries!.find((q) => q.displayName === "ReaderScreen")!;
|
||||
declaration.importedFragments = [
|
||||
{ packageRevisionId: provider.revisionId, packageAlias: "CommentRows", fragmentId: "row", variables: {} },
|
||||
];
|
||||
const document = (await readReaderFixture("queries/reader-screen.graphql")).replace(
|
||||
"_camino { ref } content { text tags }",
|
||||
"...CommentRow",
|
||||
);
|
||||
const checked = await compileQuery(
|
||||
declaration,
|
||||
workspace.interfaceImports,
|
||||
async () => document,
|
||||
async () => provider.checkedQueries!.find((q) => q.declaration.id === "row")!,
|
||||
);
|
||||
consumer.checkedQueries = consumer.checkedQueries!.map((q) => (q.declaration.id === declaration.id ? checked : q));
|
||||
const plan = compileExecutionPlan(workspace);
|
||||
const program = compileQueryProgram(plan) as {
|
||||
queries: { id: string; fragment?: { package: string; id: string }; selection: unknown }[];
|
||||
};
|
||||
assert.deepEqual(program.queries.find((q) => q.fragment)?.fragment, {
|
||||
package: provider.revisionId,
|
||||
id: "row",
|
||||
digest: provider.checkedQueries!.find((q) => q.declaration.id === "row")!.definitionDigest,
|
||||
});
|
||||
const screen = program.queries.find((q) => q.id === JSON.stringify([consumer.revisionId, declaration.id]))!;
|
||||
assert.match(JSON.stringify(screen.selection), /fragmentSpread/);
|
||||
const generated = generatePackageQueryClientBindings(consumer, undefined, workspace.packageImports);
|
||||
assert.match(generated, /"CommentRows": \{"Row": definePackageFragment/);
|
||||
assert.ok(generated.includes(JSON.stringify(comment.revisionId)));
|
||||
assert.ok(!generated.includes('"Row$inputs": definePackageQuery'));
|
||||
});
|
||||
Reference in New Issue
Block a user