Add checked workspace-interface query roots
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.
This commit is contained in:
@@ -45,11 +45,11 @@ test("generic query templates check declared bounds before closed specialization
|
||||
}`,
|
||||
[facts],
|
||||
);
|
||||
const build = (bound: string) =>
|
||||
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 Collection<Item>
|
||||
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>;
|
||||
}`,
|
||||
@@ -93,12 +93,33 @@ test("generic query templates check declared bounds before closed specialization
|
||||
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 = "") {
|
||||
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 Tasks document "queries/upcoming.graphql" operation "Upcoming" {
|
||||
query Upcoming id "upcoming" root ${root} document "queries/upcoming.graphql" operation "Upcoming" {
|
||||
fragments "queries/row.graphql"; max rows 30; ${clauses}
|
||||
}
|
||||
}`,
|
||||
@@ -119,6 +140,33 @@ package Queries id "queries" revision "queries@1" {
|
||||
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 }
|
||||
|
||||
Reference in New Issue
Block a user