Implement query execution, scoped RPC enrichment, live collections, and scaffold integration
This commit is contained in:
Vendored
+117
@@ -0,0 +1,117 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { compileCapabilityResourceSource, compileCapabilitySource } from "../../src/capability-language/index.js";
|
||||
import { compileQuery } from "../../src/query/compile.js";
|
||||
import { linkQueries } from "../../src/query/link.js";
|
||||
import type { InterfaceRevision, PackageRevision } from "../../src/capability-model/types.js";
|
||||
|
||||
export const queryFixtureSource = {
|
||||
repository: "https://query-fixture.example.test/source.git",
|
||||
commit: "1".repeat(40),
|
||||
};
|
||||
export async function queryWorkspaceFixture() {
|
||||
const interfaces = new Map<string, InterfaceRevision>();
|
||||
const sources: Record<string, string> = {};
|
||||
function iface(name: string, members: string, parameters = "") {
|
||||
const text = `${[...interfaces.keys()].map((name) => `import interface ${name};`).join("\n")}
|
||||
interface ${name}${parameters} id "${name}" revision "${name}@1" {${members}}`;
|
||||
sources[name] = text;
|
||||
const result = compileCapabilityResourceSource(text, {
|
||||
source: queryFixtureSource,
|
||||
environment: { interfaces, interfaceClosure: [...interfaces.values()] },
|
||||
});
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
if (result.resource.kind !== "interface") throw new Error("interface expected");
|
||||
interfaces.set(name, result.resource.revision);
|
||||
}
|
||||
iface("PersonFacts", `queryable value name id "name" : string {get id "name:get";}`);
|
||||
iface(
|
||||
"TaskFacts",
|
||||
`
|
||||
queryable value title id "title" : string {get id "title:get"; set id "title:set";}
|
||||
queryable value done id "done" : bool {get id "done:get";}
|
||||
queryable value rank id "rank" : int64 {get id "rank:get";}
|
||||
queryable relation assignee id "assignee" : optional-one interface PersonFacts {resolve id "assignee:resolve";}
|
||||
queryable rpc value score id "score" : int32 {get id "score:get";}
|
||||
`,
|
||||
);
|
||||
iface(
|
||||
"Collection",
|
||||
`queryable relation items id "items" : many object Item ordered {resolve id "items:resolve";}`,
|
||||
"<object Item implements TaskFacts>",
|
||||
);
|
||||
const packageSource = `${[...interfaces.keys()].map((name) => `import interface ${name};`).join("\n")}
|
||||
package Queries id "queries" revision "queries@1" {
|
||||
operation titleSet id "title-set" : string -> unit mode call receiver any
|
||||
requires {state title id "title-port" : string [write];};
|
||||
operation score id "score" : unit -> int32 mode call receiver any;
|
||||
query Upcoming id "upcoming" root Collection<interface TaskFacts> document "upcoming.graphql" operation "Upcoming" {
|
||||
fragments "row.graphql"; max rows 30; watch;
|
||||
}
|
||||
query Enriched id "enriched" root Collection<interface TaskFacts> document "enriched.graphql" operation "Enriched" {
|
||||
max rows 30; allow TaskFacts.score select "Small visible page only";
|
||||
}
|
||||
query Ranked id "ranked" root Collection<interface TaskFacts> document "ranked.graphql" operation "Ranked" {
|
||||
max rows 30; max candidates 60;
|
||||
allow TaskFacts.score predicate "Bounded local collection";
|
||||
allow TaskFacts.score order "Bounded local collection";
|
||||
}
|
||||
}`;
|
||||
sources.Queries = packageSource;
|
||||
const pkgResult = compileCapabilityResourceSource(packageSource, {
|
||||
source: queryFixtureSource,
|
||||
environment: { interfaces, interfaceClosure: [...interfaces.values()] },
|
||||
});
|
||||
assert.ok(pkgResult.ok, JSON.stringify(pkgResult.diagnostics));
|
||||
if (pkgResult.resource.kind !== "package") throw new Error("package expected");
|
||||
const pkg: PackageRevision = pkgResult.resource.revision;
|
||||
const allInterfaces = [...interfaces.values(), ...(pkgResult.resource.specializations ?? [])];
|
||||
const documents: Record<string, string> = {
|
||||
"upcoming.graphql": `query Upcoming($first: Int!, $after: Cursor) {root {items(first: $first, after: $after, where: {done: {eq: false}}, orderBy: [{rank: ASC}]) {entries {key cursor node {_qx {ref} ...TaskRow}} pageInfo {hasNextPage endCursor}}}}`,
|
||||
"row.graphql": `fragment TaskRow on TaskFacts {title rank done assignee {name}}`,
|
||||
"enriched.graphql": `query Enriched {root {items(first: 3) {entries {key node {_qx {ref} title score}}}}}`,
|
||||
"ranked.graphql": `query Ranked {root {items(first: 3, where: {score: {gt: 0}}, orderBy: [{score: DESC}]) {entries {key node {_qx {ref} title}}}}}`,
|
||||
};
|
||||
pkg.checkedQueries = await Promise.all(
|
||||
pkg.queries!.map((query) => compileQuery(query, allInterfaces, async (name) => documents[name]!)),
|
||||
);
|
||||
const implementation = (atom: string) => `conform ${atom} as TaskFacts id "${atom}-facts" {
|
||||
private state Title${atom} id "${atom}:title" on ${atom} : string policy crdt(string) default "Untitled";
|
||||
private state Done${atom} id "${atom}:done" on ${atom} : bool policy optimistic-register default false;
|
||||
private state Rank${atom} id "${atom}:rank" on ${atom} : int64 policy optimistic-register default 0;
|
||||
private edge Assignee${atom} id "${atom}:assignee" {
|
||||
atom ${atom} projection assignee id "${atom}:assignee:forward" optional-one;
|
||||
interface PersonFacts projection tasks id "${atom}:assignee:inverse" many;
|
||||
}
|
||||
bind title.get to state Title${atom}.read;
|
||||
bind title.set to package Queries.titleSet with {title to state Title${atom};};
|
||||
bind done to state Done${atom}; bind rank to state Rank${atom};
|
||||
bind assignee.resolve to edge Assignee${atom}.assignee.resolve;
|
||||
bind score.get to package Queries.score query-reason "Explicit bounded score computation";
|
||||
}`;
|
||||
const workspaceSource = `workspace QueryFixture id "query-fixture" revision "query-fixture@1" commit "${queryFixtureSource.commit}" {
|
||||
import interface PersonFacts; import interface TaskFacts; import interface Collection; import package Queries;
|
||||
atom Task id "Task"; atom Reminder id "Reminder"; atom Person id "Person"; atom Tasks id "Tasks";
|
||||
conform Person as PersonFacts id "person-facts" {
|
||||
private state Name id "person:name" on Person : string policy optimistic-register default "Nobody";
|
||||
bind name to state Name;
|
||||
}
|
||||
${implementation("Task")} ${implementation("Reminder")}
|
||||
conform Tasks as Collection<interface TaskFacts> id "tasks-collection" {
|
||||
private edge Items id "items" {
|
||||
atom Tasks projection items id "items:forward" many ordered;
|
||||
interface TaskFacts projection collections id "items:inverse" many;
|
||||
}
|
||||
bind items.resolve to edge Items.items.resolve;
|
||||
}
|
||||
}`;
|
||||
sources.workspace = workspaceSource;
|
||||
const result = compileCapabilitySource(workspaceSource, "workspace.qx", {
|
||||
interfaces,
|
||||
interfaceClosure: allInterfaces,
|
||||
packages: new Map([["Queries", pkg]]),
|
||||
packageClosure: [pkg],
|
||||
});
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
const workspace = { ...result.workspace, linkedQueries: linkQueries(result.workspace) };
|
||||
return { workspace, pkg, interfaces: allInterfaces, sources, documents };
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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 type { InterfaceRevision } from "../src/capability-model/types.js";
|
||||
|
||||
const source = { repository: "https://example.test/queries.git", commit: "a".repeat(40) };
|
||||
@@ -33,6 +34,64 @@ interface Tasks id "tasks" revision "tasks@1" {
|
||||
}`,
|
||||
[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) =>
|
||||
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 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,
|
||||
);
|
||||
});
|
||||
function fixture(clauses = "") {
|
||||
const result = compileCapabilityResourceSource(
|
||||
`import interface Tasks; import interface TaskFacts;
|
||||
@@ -66,6 +125,40 @@ const document = `query Upcoming($first: Int!, $before: Int64!) {
|
||||
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("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, {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { queryWorkspaceFixture } from "./fixtures/query-workspace.js";
|
||||
import { linkQueries } from "../src/query/link.js";
|
||||
|
||||
test("query fixture links generic collections, both implementations, and native getter/RPC setter", async () => {
|
||||
const { workspace } = await queryWorkspaceFixture();
|
||||
const query = workspace.linkedQueries.find((query) => query.id.endsWith(":upcoming"))!;
|
||||
assert.ok(query);
|
||||
assert.equal(query.fields.filter((field) => field.memberId === "title").length, 2);
|
||||
assert.ok(
|
||||
query.fields.filter((field) => field.memberId === "title").every((field) => field.binding.kind === "state"),
|
||||
);
|
||||
const broken = structuredClone(workspace);
|
||||
const facts = broken.interfaceImports.find((iface) => iface.displayName === "TaskFacts")!;
|
||||
const title = facts.members.find((member) => member.id === "title")!;
|
||||
assert.equal(title.kind, "value");
|
||||
delete title.queryRead;
|
||||
assert.throws(() => linkQueries(broken), /changed/);
|
||||
});
|
||||
Reference in New Issue
Block a user