Restore generic Studio object navigation and discovery
This commit is contained in:
@@ -381,6 +381,27 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
|
|||||||
query.id,
|
query.id,
|
||||||
"Local and replica queries must use the same root contract",
|
"Local and replica queries must use the same root contract",
|
||||||
);
|
);
|
||||||
|
if (localRoot.kind === "workspace-interface") {
|
||||||
|
const sourceWindows = source.query.checked.selection
|
||||||
|
.flatMap((root) => root.selection)
|
||||||
|
.filter((s) => s.name === "objects");
|
||||||
|
for (const selection of query.query.checked.selection
|
||||||
|
.flatMap((root) => root.selection)
|
||||||
|
.filter((s) => s.name === "objects")) {
|
||||||
|
if (
|
||||||
|
!sourceWindows.some(
|
||||||
|
(window) =>
|
||||||
|
canonicalJson({ arguments: window.arguments, conditions: window.conditions }) ===
|
||||||
|
canonicalJson({ arguments: selection.arguments, conditions: selection.conditions }),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
throw new ExecutionPlanError(
|
||||||
|
"REPLICA_WINDOW_MISMATCH",
|
||||||
|
query.id,
|
||||||
|
"Local objects must observe the identical confirmed source window; use availableObjects for a local candidate view",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
const traversals = (linked: LinkedQuery) => {
|
const traversals = (linked: LinkedQuery) => {
|
||||||
const paths = new Map<string, { complete: boolean; requiresComplete: boolean; windows: Set<string> }>();
|
const paths = new Map<string, { complete: boolean; requiresComplete: boolean; windows: Set<string> }>();
|
||||||
const entry = (key: string) => {
|
const entry = (key: string) => {
|
||||||
|
|||||||
+4
-1
@@ -27,7 +27,7 @@ export function queryRootSchema(root: QueryRoot, interfaces: readonly InterfaceR
|
|||||||
{
|
{
|
||||||
kind: "relationship",
|
kind: "relationship",
|
||||||
id: workspaceObjectsMemberId,
|
id: workspaceObjectsMemberId,
|
||||||
displayName: local ? "availableObjects" : "objects",
|
displayName: "objects",
|
||||||
target: { kind: "interface", interfaceRevisionId: root.interface },
|
target: { kind: "interface", interfaceRevisionId: root.interface },
|
||||||
cardinality: "many-unique",
|
cardinality: "many-unique",
|
||||||
ordered: false,
|
ordered: false,
|
||||||
@@ -36,5 +36,8 @@ export function queryRootSchema(root: QueryRoot, interfaces: readonly InterfaceR
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
// Local objects observes the confirmed server window. availableObjects is the
|
||||||
|
// separate opt-in view that also includes pending local candidates.
|
||||||
|
if (local) synthetic.members.push({ ...synthetic.members[0]!, displayName: "availableObjects" });
|
||||||
return { interface: synthetic.revisionId, interfaces: [...interfaces, synthetic] };
|
return { interface: synthetic.revisionId, interfaces: [...interfaces, synthetic] };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ package Reader id "package:reader" revision "package:reader@1" {
|
|||||||
assert.ok(transaction.ok, JSON.stringify(transaction.diagnostics));
|
assert.ok(transaction.ok, JSON.stringify(transaction.diagnostics));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("query placement round-trips and local extents never claim complete membership", async () => {
|
test("query placement distinguishes confirmed windows from local candidate extents", async () => {
|
||||||
const { compileQuery } = await import("../src/query/compile.js");
|
const { compileQuery } = await import("../src/query/compile.js");
|
||||||
const { linkQueries } = await import("../src/query/link.js");
|
const { linkQueries } = await import("../src/query/link.js");
|
||||||
const { toBinary, fromBinary, fromJson } = await import("@bufbuild/protobuf");
|
const { toBinary, fromBinary, fromJson } = await import("@bufbuild/protobuf");
|
||||||
@@ -125,10 +125,8 @@ package Reader id "package:reader" revision "package:reader@1" {
|
|||||||
await compileQuery(admission, [iface], async () => document("Visible", "objects")),
|
await compileQuery(admission, [iface], async () => document("Visible", "objects")),
|
||||||
await compileQuery(local, [iface], async () => document("Presentation", "availableObjects")),
|
await compileQuery(local, [iface], async () => document("Presentation", "availableObjects")),
|
||||||
];
|
];
|
||||||
await assert.rejects(
|
const confirmed = await compileQuery(local, [iface], async () => document("Presentation", "objects"));
|
||||||
compileQuery(local, [iface], async () => document("Presentation", "objects")),
|
assert.equal(confirmed.selection[0]!.selection[0]!.name, "objects");
|
||||||
/Cannot query field "objects"/,
|
|
||||||
);
|
|
||||||
const remote = structuredClone(iface);
|
const remote = structuredClone(iface);
|
||||||
delete remote.members[0]!.operations[0]!.execution;
|
delete remote.members[0]!.operations[0]!.execution;
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
|
|||||||
@@ -18,3 +18,17 @@ test("local relationship pages may reuse exactly the confirmed source window", a
|
|||||||
});
|
});
|
||||||
assert.doesNotThrow(() => compileExecutionPlan(workspace));
|
assert.doesNotThrow(() => compileExecutionPlan(workspace));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("local workspace objects reads the exact server window without adding local candidates", async () => {
|
||||||
|
const load = (first: number) =>
|
||||||
|
loadReaderWorkspace((name, text) => {
|
||||||
|
if (name === "ReaderRuntime.qx") return text.replace(/^.*query HighlightList id.*$/m, "");
|
||||||
|
if (name === "queries/highlight-list.graphql")
|
||||||
|
return text.replace("availableObjects(all: 200", `objects(first: ${first}`);
|
||||||
|
return text;
|
||||||
|
});
|
||||||
|
const workspace = await load(2);
|
||||||
|
assert.doesNotThrow(() => compileExecutionPlan(workspace));
|
||||||
|
const wrong = await load(3);
|
||||||
|
assert.throws(() => compileExecutionPlan(wrong), /REPLICA_WINDOW_MISMATCH/);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user