Standardize Camino query references and retire live field sidecars
This commit is contained in:
@@ -76,13 +76,10 @@ export function generatePortableClientBindings(
|
|||||||
const operations = query.operations.map(
|
const operations = query.operations.map(
|
||||||
(key) => plan.definition.operations.find((operation) => operation.key === key)!,
|
(key) => plan.definition.operations.find((operation) => operation.key === key)!,
|
||||||
);
|
);
|
||||||
const live = (selections: readonly QuerySelection[]): boolean =>
|
|
||||||
selections.some((selection) => !!selection.liveSelectionId || live(selection.selection));
|
|
||||||
if (
|
if (
|
||||||
operations.some(
|
operations.some(
|
||||||
(operation) => operation.execution.kind !== "snapshot" || !operation.execution.targets.includes("server"),
|
(operation) => operation.execution.kind !== "snapshot" || !operation.execution.targets.includes("server"),
|
||||||
) ||
|
)
|
||||||
live(query.query.checked.selection)
|
|
||||||
) {
|
) {
|
||||||
unavailable.push({ key: query.id, reason: "QUERY_NOT_SNAPSHOT_PROJECTION" });
|
unavailable.push({ key: query.id, reason: "QUERY_NOT_SNAPSHOT_PROJECTION" });
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
+1
-19
@@ -1,6 +1,5 @@
|
|||||||
import type { BindingSchema } from "./index.js";
|
import type { BindingSchema } from "./index.js";
|
||||||
import type { ValueType } from "../capability-model/types.js";
|
import type { ValueType } from "../capability-model/types.js";
|
||||||
import { queryPresentation } from "../query/presentation.js";
|
|
||||||
|
|
||||||
/** Browser projection of the SAME checked RPC types, not a second props schema. */
|
/** Browser projection of the SAME checked RPC types, not a second props schema. */
|
||||||
export function generateReactBindings(
|
export function generateReactBindings(
|
||||||
@@ -131,25 +130,9 @@ export function generateReactBindings(
|
|||||||
return type(value);
|
return type(value);
|
||||||
};
|
};
|
||||||
const queries = pkg.checkedQueries ?? [];
|
const queries = pkg.checkedQueries ?? [];
|
||||||
const presentedType = (
|
|
||||||
value: ValueType,
|
|
||||||
fields: ReturnType<typeof queryPresentation>,
|
|
||||||
path: string[] = [],
|
|
||||||
): string => {
|
|
||||||
const field = fields.find((field) => JSON.stringify(field.path) === JSON.stringify(path));
|
|
||||||
if (field)
|
|
||||||
return `${field.conditions.length ? "(" : ""}${field.setOperationId ? "WritableQueryField" : "QueryField"}<${type(field.valueType)}>${field.conditions.length ? " | null)" : ""}`;
|
|
||||||
if (value.kind === "optional") return `(${presentedType(value.value, fields, path)} | null)`;
|
|
||||||
if (value.kind === "list") return `Array<${presentedType(value.value, fields, [...path, "*"])}>`;
|
|
||||||
if (value.kind === "record")
|
|
||||||
return `{${Object.entries(value.fields)
|
|
||||||
.map(([key, child]) => `${q(key)}: ${presentedType(child, fields, [...path, key])}`)
|
|
||||||
.join(";")}}`;
|
|
||||||
return queryType(value);
|
|
||||||
};
|
|
||||||
const queryCode =
|
const queryCode =
|
||||||
`export type QueryVariables = {${queries.map((query) => `${q(query.declaration.displayName)}: ${queryType(query.variables)}`).join(";")}};\n` +
|
`export type QueryVariables = {${queries.map((query) => `${q(query.declaration.displayName)}: ${queryType(query.variables)}`).join(";")}};\n` +
|
||||||
`export type QueryResults = {${queries.map((query) => `${q(query.declaration.displayName)}: ${presentedType(query.output, queryPresentation(query, schema.interfaces))}`).join(";")}};\n` +
|
`export type QueryResults = {${queries.map((query) => `${q(query.declaration.displayName)}: ${queryType(query.output)}`).join(";")}};\n` +
|
||||||
`export const queries: {${queries.map((query) => `${q(query.declaration.displayName)}: QueryDescriptor<QueryVariables[${q(query.declaration.displayName)}], QueryResults[${q(query.declaration.displayName)}], ${q(query.declaration.root.kind)}>`).join(";")}} = ${JSON.stringify(
|
`export const queries: {${queries.map((query) => `${q(query.declaration.displayName)}: QueryDescriptor<QueryVariables[${q(query.declaration.displayName)}], QueryResults[${q(query.declaration.displayName)}], ${q(query.declaration.root.kind)}>`).join(";")}} = ${JSON.stringify(
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
queries.map((query) => [
|
queries.map((query) => [
|
||||||
@@ -160,7 +143,6 @@ export function generateReactBindings(
|
|||||||
root: query.declaration.root,
|
root: query.declaration.root,
|
||||||
variables: query.variables,
|
variables: query.variables,
|
||||||
output: query.output,
|
output: query.output,
|
||||||
liveFields: queryPresentation(query, schema.interfaces),
|
|
||||||
watch: query.declaration.watch,
|
watch: query.declaration.watch,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function validateControllers(
|
|||||||
objects = selected(root?.selection, "objects"),
|
objects = selected(root?.selection, "objects"),
|
||||||
entries = selected(objects?.selection, "entries"),
|
entries = selected(objects?.selection, "entries"),
|
||||||
node = selected(entries?.selection, "node"),
|
node = selected(entries?.selection, "node"),
|
||||||
qx = selected(node?.selection, "_qx"),
|
qx = selected(node?.selection, "_camino"),
|
||||||
ref = selected(qx?.selection, "ref");
|
ref = selected(qx?.selection, "ref");
|
||||||
const all = objects?.arguments.all;
|
const all = objects?.arguments.all;
|
||||||
if (
|
if (
|
||||||
@@ -44,7 +44,7 @@ export function validateControllers(
|
|||||||
)
|
)
|
||||||
fail(
|
fail(
|
||||||
`${path}.registry`,
|
`${path}.registry`,
|
||||||
"Controller registry requires a native/portable workspace query over its subject, no variables, and root.objects(all: 1..64).entries.node._qx.ref without aliases or conditional selections",
|
"Controller registry requires a native/portable workspace query over its subject, no variables, and root.objects(all: 1..64).entries.node._camino.ref without aliases or conditional selections",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const iface = interfaces.find((i) => i.revisionId === controller.subject && !i.template);
|
const iface = interfaces.find((i) => i.revisionId === controller.subject && !i.template);
|
||||||
|
|||||||
+11
-29
@@ -87,6 +87,16 @@ export async function compileQuery(
|
|||||||
definitions.push(...document.definitions);
|
definitions.push(...document.definitions);
|
||||||
}
|
}
|
||||||
const document: DocumentNode = { kind: "Document" as DocumentNode["kind"], definitions };
|
const document: DocumentNode = { kind: "Document" as DocumentNode["kind"], definitions };
|
||||||
|
visit(document, {
|
||||||
|
Directive(node) {
|
||||||
|
if (node.name.value === "live")
|
||||||
|
fail(
|
||||||
|
"QUERY_RETIRED_DIRECTIVE",
|
||||||
|
"@live is retired. Observations watch ordinary query values; use checked operations for mutations.",
|
||||||
|
node,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
const generated = querySchema(declaration, interfaces, document);
|
const generated = querySchema(declaration, interfaces, document);
|
||||||
const operations = definitions.filter((entry) => entry.kind === "OperationDefinition");
|
const operations = definitions.filter((entry) => entry.kind === "OperationDefinition");
|
||||||
if (
|
if (
|
||||||
@@ -201,16 +211,7 @@ export async function compileQuery(
|
|||||||
Field(node) {
|
Field(node) {
|
||||||
const parent = info.getParentType();
|
const parent = info.getParentType();
|
||||||
const contract = parent && generated.byName.get(parent.name);
|
const contract = parent && generated.byName.get(parent.name);
|
||||||
if (node.directives?.some((directive) => directive.name.value === "live")) {
|
if (!contract || node.name.value === "_camino") return;
|
||||||
const member = contract?.members.find((entry) => entry.displayName === node.name.value);
|
|
||||||
if (!member || member.kind !== "value" || member.queryRead?.execution !== "native")
|
|
||||||
fail(
|
|
||||||
"QUERY_LIVE_UNSUPPORTED",
|
|
||||||
"@live requires a direct native-queryable value field, not RPC enrichment or a synthetic result",
|
|
||||||
node,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!contract || node.name.value === "_qx") return;
|
|
||||||
mark(contract.revisionId, node.name.value, "select", node);
|
mark(contract.revisionId, node.name.value, "select", node);
|
||||||
const member = contract.members.find((entry) => entry.displayName === node.name.value)!;
|
const member = contract.members.find((entry) => entry.displayName === node.name.value)!;
|
||||||
if (member.kind !== "relationship" || (member.cardinality !== "many" && member.cardinality !== "many-unique"))
|
if (member.kind !== "relationship" || (member.cardinality !== "many" && member.cardinality !== "many-unique"))
|
||||||
@@ -425,13 +426,6 @@ export async function compileQuery(
|
|||||||
{
|
{
|
||||||
name: node.name.value,
|
name: node.name.value,
|
||||||
key: node.alias?.value ?? node.name.value,
|
key: node.alias?.value ?? node.name.value,
|
||||||
...(node.directives?.some((directive) => directive.name.value === "live")
|
|
||||||
? {
|
|
||||||
liveSelectionId: createHash("sha256")
|
|
||||||
.update(JSON.stringify([node.loc?.source.name, node.loc?.start, contract?.revisionId, member?.id]))
|
|
||||||
.digest("hex"),
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
...(member && contract ? { interfaceRevisionId: contract.revisionId, memberId: member.id } : {}),
|
...(member && contract ? { interfaceRevisionId: contract.revisionId, memberId: member.id } : {}),
|
||||||
...(member?.kind === "relationship" ? { targetInterfaceRevisionId: generated.target(member) } : {}),
|
...(member?.kind === "relationship" ? { targetInterfaceRevisionId: generated.target(member) } : {}),
|
||||||
conditions: [...inherited, ...conditions(node.directives)],
|
conditions: [...inherited, ...conditions(node.directives)],
|
||||||
@@ -447,18 +441,6 @@ export async function compileQuery(
|
|||||||
const normalized = print(document),
|
const normalized = print(document),
|
||||||
schema = printSchema(generated.schema);
|
schema = printSchema(generated.schema);
|
||||||
const selections = selection(operations[0]!.selectionSet, generated.schema.getQueryType()!);
|
const selections = selection(operations[0]!.selectionSet, generated.schema.getQueryType()!);
|
||||||
const checkPresentation = (entries: QuerySelection[]) => {
|
|
||||||
for (const key of new Set(entries.map((entry) => entry.key))) {
|
|
||||||
const siblings = entries.filter((entry) => entry.key === key);
|
|
||||||
if (siblings.some((entry) => entry.liveSelectionId) && siblings.some((entry) => !entry.liveSelectionId))
|
|
||||||
throw new QueryCompileError(
|
|
||||||
"QUERY_LIVE_CONFLICT",
|
|
||||||
`${key} mixes live and plain selections; use separate aliases`,
|
|
||||||
);
|
|
||||||
checkPresentation(siblings.flatMap((entry) => entry.selection));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
checkPresentation(selections);
|
|
||||||
const definitionDigest = createHash("sha256")
|
const definitionDigest = createHash("sha256")
|
||||||
.update(
|
.update(
|
||||||
canonicalJson({
|
canonicalJson({
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import type { InterfaceRevision, ValueType } from "../capability-model/types.js";
|
|
||||||
import type { CheckedQuery, QuerySelection } from "./types.js";
|
|
||||||
|
|
||||||
/** Browser presentation is separate from the portable query value contract. */
|
|
||||||
export function queryPresentation(query: CheckedQuery, interfaces: readonly InterfaceRevision[]) {
|
|
||||||
const result: {
|
|
||||||
path: string[];
|
|
||||||
selectionId: string;
|
|
||||||
interfaceRevisionId: string;
|
|
||||||
getOperationId: string;
|
|
||||||
setOperationId?: string;
|
|
||||||
watchOperationId?: string;
|
|
||||||
inputKind: "value" | "fields";
|
|
||||||
valueType: ValueType;
|
|
||||||
conditions: (QuerySelection["conditions"][number] & { defaultValue?: boolean })[];
|
|
||||||
}[] = [];
|
|
||||||
const visit = (type: ValueType, selections: QuerySelection[], path: string[]) => {
|
|
||||||
if (type.kind === "optional") return visit(type.value, selections, path);
|
|
||||||
if (type.kind === "list") return visit(type.value, selections, [...path, "*"]);
|
|
||||||
if (type.kind !== "record") return;
|
|
||||||
for (const [name, field] of Object.entries(type.fields)) {
|
|
||||||
const matches = selections.filter((entry) => entry.key === name);
|
|
||||||
if (matches.some((entry) => entry.liveSelectionId) && matches.some((entry) => !entry.liveSelectionId))
|
|
||||||
throw new Error(`QUERY_LIVE_CONFLICT: ${[...path, name].join(".")} mixes live and plain selections`);
|
|
||||||
for (const entry of matches) {
|
|
||||||
if (entry.liveSelectionId) {
|
|
||||||
const iface = interfaces.find((iface) => iface.revisionId === entry.interfaceRevisionId)!;
|
|
||||||
const member = iface.members.find((member) => member.id === entry.memberId)!;
|
|
||||||
if (member.kind !== "value") throw new Error("QUERY_LIVE_UNSUPPORTED");
|
|
||||||
const get = member.operations.find((op) => op.displayName === "get")!;
|
|
||||||
const set = member.operations.find((op) => op.displayName === "set");
|
|
||||||
result.push({
|
|
||||||
path: [...path, name],
|
|
||||||
selectionId: entry.liveSelectionId,
|
|
||||||
interfaceRevisionId: iface.revisionId,
|
|
||||||
getOperationId: get.id,
|
|
||||||
setOperationId: set?.id,
|
|
||||||
watchOperationId: member.operations.find((op) => op.displayName === "watch-start")?.id,
|
|
||||||
inputKind: set?.inputType.kind === "record" ? "fields" : "value",
|
|
||||||
valueType: get.outputType,
|
|
||||||
conditions: entry.conditions.map((condition) => {
|
|
||||||
const fallback =
|
|
||||||
condition.value.kind === "variable" ? query.variableDefaults[condition.value.name] : undefined;
|
|
||||||
return {
|
|
||||||
...condition,
|
|
||||||
...(fallback?.kind === "literal" && typeof fallback.value === "boolean"
|
|
||||||
? { defaultValue: fallback.value }
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} else visit(field, entry.selection, [...path, name]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
visit(query.output, query.selection, []);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -94,7 +94,8 @@ export function relationalCompiler(
|
|||||||
if (!prefix(base, path)) return false;
|
if (!prefix(base, path)) return false;
|
||||||
// Object identity does not determine which incoming membership carried it.
|
// Object identity does not determine which incoming membership carried it.
|
||||||
return (
|
return (
|
||||||
last === "entry" || !(path[base.length] === "_qx" && ["entry", "mapKey"].includes(path[base.length + 1] ?? ""))
|
last === "entry" ||
|
||||||
|
!(path[base.length] === "_camino" && ["entry", "mapKey"].includes(path[base.length + 1] ?? ""))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
if (!ok)
|
if (!ok)
|
||||||
@@ -153,7 +154,7 @@ export function relationalCompiler(
|
|||||||
available?: Availability,
|
available?: Availability,
|
||||||
): QueryExpression => {
|
): QueryExpression => {
|
||||||
requireAvailable(names, available, node);
|
requireAvailable(names, available, node);
|
||||||
const meta = names.at(-2) === "_qx";
|
const meta = names.at(-2) === "_camino";
|
||||||
let fieldIndex = names.length - 1;
|
let fieldIndex = names.length - 1;
|
||||||
if (!meta) {
|
if (!meta) {
|
||||||
let cursor = root;
|
let cursor = root;
|
||||||
@@ -334,10 +335,10 @@ export function relationalCompiler(
|
|||||||
children.push({ kind: "not", child: { kind: "and", children: children.splice(before) } });
|
children.push({ kind: "not", child: { kind: "and", children: children.splice(before) } });
|
||||||
} else if (current.kind === "pair" && (name === "source" || name === "target"))
|
} else if (current.kind === "pair" && (name === "source" || name === "target"))
|
||||||
visit(current[name], field.value, [...base, name]);
|
visit(current[name], field.value, [...base, name]);
|
||||||
else if (name === "_qx") {
|
else if (name === "_camino") {
|
||||||
for (const meta of pairs(field.value)) {
|
for (const meta of pairs(field.value)) {
|
||||||
if (meta.name.value !== "relations") {
|
if (meta.name.value !== "relations") {
|
||||||
const expr = expression(row, [...base, "_qx", meta.name.value], ["predicate"], meta, available);
|
const expr = expression(row, [...base, "_camino", meta.name.value], ["predicate"], meta, available);
|
||||||
children.push(
|
children.push(
|
||||||
...(comparisons(meta.value, (operator, value) => ({
|
...(comparisons(meta.value, (operator, value) => ({
|
||||||
kind: "compare",
|
kind: "compare",
|
||||||
@@ -531,7 +532,7 @@ export function relationalCompiler(
|
|||||||
expression: { ...k.expression, path: [{ kind: "source" as const }, ...k.expression.path] },
|
expression: { ...k.expression, path: [{ kind: "source" as const }, ...k.expression.path] },
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
path: ["target", "_qx", "entry"],
|
path: ["target", "_camino", "entry"],
|
||||||
expression: {
|
expression: {
|
||||||
path: [{ kind: "target" as const }],
|
path: [{ kind: "target" as const }],
|
||||||
leaf: { kind: "entry" as const },
|
leaf: { kind: "entry" as const },
|
||||||
|
|||||||
@@ -205,7 +205,9 @@ export function relationalSchema(env: RelationalSchemaEnvironment, document?: Do
|
|||||||
metadata[name] = { type: !operator && nullable ? t : new GraphQLNonNull(t) };
|
metadata[name] = { type: !operator && nullable ? t : new GraphQLNonNull(t) };
|
||||||
}
|
}
|
||||||
if (Object.keys(metadata).length)
|
if (Object.keys(metadata).length)
|
||||||
result._qx = { type: new GraphQLNonNull(out("ColumnsMeta", row, () => metadata, `${operator}:${nullable}`)) };
|
result._camino = {
|
||||||
|
type: new GraphQLNonNull(out("ColumnsMeta", row, () => metadata, `${operator}:${nullable}`)),
|
||||||
|
};
|
||||||
// A type may have no matching scalar at this node; a private schema sentinel
|
// A type may have no matching scalar at this node; a private schema sentinel
|
||||||
// keeps it valid while compiler validation forbids selecting that sentinel.
|
// keeps it valid while compiler validation forbids selecting that sentinel.
|
||||||
if (!Object.keys(result).length) result._unavailable = { type: GraphQLBoolean };
|
if (!Object.keys(result).length) result._unavailable = { type: GraphQLBoolean };
|
||||||
@@ -233,7 +235,7 @@ export function relationalSchema(env: RelationalSchemaEnvironment, document?: Do
|
|||||||
else if (queryKeyType(field.type)) result[name] = { type: GraphQLBoolean };
|
else if (queryKeyType(field.type)) result[name] = { type: GraphQLBoolean };
|
||||||
}
|
}
|
||||||
const meta = Object.fromEntries([...leafFields(row)].map(([name]) => [name, { type: GraphQLBoolean }]));
|
const meta = Object.fromEntries([...leafFields(row)].map(([name]) => [name, { type: GraphQLBoolean }]));
|
||||||
if (Object.keys(meta).length) result._qx = { type: inp("KeyMeta", row, () => meta) };
|
if (Object.keys(meta).length) result._camino = { type: inp("KeyMeta", row, () => meta) };
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
const comparisons = (
|
const comparisons = (
|
||||||
@@ -266,7 +268,7 @@ export function relationalSchema(env: RelationalSchemaEnvironment, document?: Do
|
|||||||
if (!operator || operator === "countPresent")
|
if (!operator || operator === "countPresent")
|
||||||
meta[name] = { type: ordering ? env.direction : compare(operator ? aggregateType(operator, type) : type) };
|
meta[name] = { type: ordering ? env.direction : compare(operator ? aggregateType(operator, type) : type) };
|
||||||
if (Object.keys(meta).length)
|
if (Object.keys(meta).length)
|
||||||
result._qx = { type: inp("ExpressionMeta", row, () => meta, `${operator}:${ordering}`) };
|
result._camino = { type: inp("ExpressionMeta", row, () => meta, `${operator}:${ordering}`) };
|
||||||
if (!Object.keys(result).length) result._unavailable = { type: GraphQLBoolean };
|
if (!Object.keys(result).length) result._unavailable = { type: GraphQLBoolean };
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
@@ -306,7 +308,7 @@ export function relationalSchema(env: RelationalSchemaEnvironment, document?: Do
|
|||||||
if (field.kind === "leaf" && !comparableValue(field.type)) continue;
|
if (field.kind === "leaf" && !comparableValue(field.type)) continue;
|
||||||
result[name] = { type: field.kind === "row" ? where(field.row) : compare(field.type) };
|
result[name] = { type: field.kind === "row" ? where(field.row) : compare(field.type) };
|
||||||
}
|
}
|
||||||
result._qx = {
|
result._camino = {
|
||||||
type: inp("WhereMeta", row, () => {
|
type: inp("WhereMeta", row, () => {
|
||||||
const meta: GraphQLInputFieldConfigMap = {};
|
const meta: GraphQLInputFieldConfigMap = {};
|
||||||
for (const [name, type] of leafFields(row)) meta[name] = { type: compare(type) };
|
for (const [name, type] of leafFields(row)) meta[name] = { type: compare(type) };
|
||||||
@@ -397,7 +399,7 @@ export function relationalSchema(env: RelationalSchemaEnvironment, document?: Do
|
|||||||
const discoverObject = (row: QueryRow, set: SelectionSetNode) => {
|
const discoverObject = (row: QueryRow, set: SelectionSetNode) => {
|
||||||
for (const node of selections(set))
|
for (const node of selections(set))
|
||||||
if (node.selectionSet) {
|
if (node.selectionSet) {
|
||||||
if (node.name.value === "_qx") {
|
if (node.name.value === "_camino") {
|
||||||
for (const meta of selections(node.selectionSet))
|
for (const meta of selections(node.selectionSet))
|
||||||
if (meta.name.value === "relations" && meta.selectionSet)
|
if (meta.name.value === "relations" && meta.selectionSet)
|
||||||
for (const edge of selections(meta.selectionSet)) {
|
for (const edge of selections(meta.selectionSet)) {
|
||||||
|
|||||||
+2
-4
@@ -11,8 +11,6 @@ import {
|
|||||||
GraphQLList,
|
GraphQLList,
|
||||||
GraphQLNonNull,
|
GraphQLNonNull,
|
||||||
GraphQLSchema,
|
GraphQLSchema,
|
||||||
GraphQLDirective,
|
|
||||||
DirectiveLocation,
|
|
||||||
specifiedDirectives,
|
specifiedDirectives,
|
||||||
type GraphQLOutputType,
|
type GraphQLOutputType,
|
||||||
type GraphQLInputType,
|
type GraphQLInputType,
|
||||||
@@ -198,7 +196,7 @@ export function querySchema(
|
|||||||
...(helpers ? { relations: { type: new GraphQLNonNull(helpers) } } : {}),
|
...(helpers ? { relations: { type: new GraphQLNonNull(helpers) } } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const fields: GraphQLFieldConfigMap<unknown, unknown> = { _qx: { type: new GraphQLNonNull(metadata) } };
|
const fields: GraphQLFieldConfigMap<unknown, unknown> = { _camino: { type: new GraphQLNonNull(metadata) } };
|
||||||
for (const member of contract(id).members) {
|
for (const member of contract(id).members) {
|
||||||
if (member.kind === "operation" || member.kind === "module" || !member.queryRead) continue;
|
if (member.kind === "operation" || member.kind === "module" || !member.queryRead) continue;
|
||||||
if (!/^[_A-Za-z][_0-9A-Za-z]*$/.test(member.displayName) || member.displayName.startsWith("_"))
|
if (!/^[_A-Za-z][_0-9A-Za-z]*$/.test(member.displayName) || member.displayName.startsWith("_"))
|
||||||
@@ -258,7 +256,7 @@ export function querySchema(
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
const schema = new GraphQLSchema({
|
const schema = new GraphQLSchema({
|
||||||
directives: [...specifiedDirectives, new GraphQLDirective({ name: "live", locations: [DirectiveLocation.FIELD] })],
|
directives: specifiedDirectives,
|
||||||
query: new GraphQLObjectType({
|
query: new GraphQLObjectType({
|
||||||
name: "QxQuery",
|
name: "QxQuery",
|
||||||
fields: { root: { type: new GraphQLNonNull(object(root.interface)) } },
|
fields: { root: { type: new GraphQLNonNull(object(root.interface)) } },
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ export type QueryArgument =
|
|||||||
| { kind: "list"; values: QueryArgument[] }
|
| { kind: "list"; values: QueryArgument[] }
|
||||||
| { kind: "object"; fields: Record<string, QueryArgument> };
|
| { kind: "object"; fields: Record<string, QueryArgument> };
|
||||||
export interface QuerySelection {
|
export interface QuerySelection {
|
||||||
liveSelectionId?: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
key: string;
|
key: string;
|
||||||
interfaceRevisionId?: InterfaceRevisionId;
|
interfaceRevisionId?: InterfaceRevisionId;
|
||||||
|
|||||||
@@ -34,16 +34,16 @@ test("controller discovery requires complete bounded accepted-state membership",
|
|||||||
const check = async (document: string) =>
|
const check = async (document: string) =>
|
||||||
parseControllerCatalog(metadata, [iface], [await compileQuery(declaration, [iface], async () => document)]);
|
parseControllerCatalog(metadata, [iface], [await compileQuery(declaration, [iface], async () => document)]);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
(await check("query Enabled {root {objects(all:64,where:{enabled:{eq:true}}) {entries {node {_qx {ref}}}}}}"))
|
(await check("query Enabled {root {objects(all:64,where:{enabled:{eq:true}}) {entries {node {_camino {ref}}}}}}"))
|
||||||
.length,
|
.length,
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
for (const document of [
|
for (const document of [
|
||||||
"query Enabled {root {objects(first:64) {entries {node {_qx {ref}}}}}}",
|
"query Enabled {root {objects(first:64) {entries {node {_camino {ref}}}}}}",
|
||||||
"query Enabled {root {objects(all:65) {entries {node {_qx {ref}}}}}}",
|
"query Enabled {root {objects(all:65) {entries {node {_camino {ref}}}}}}",
|
||||||
"query Enabled {root {devices:objects(all:64) {entries {node {_qx {ref}}}}}}",
|
"query Enabled {root {devices:objects(all:64) {entries {node {_camino {ref}}}}}}",
|
||||||
"query Enabled {root {objects(all:64) {entries {node {enabled}}}}}",
|
"query Enabled {root {objects(all:64) {entries {node {enabled}}}}}",
|
||||||
"query Enabled($enabled:Boolean!) {root {objects(all:64,where:{enabled:{eq:$enabled}}) {entries {node {_qx {ref}}}}}}",
|
"query Enabled($enabled:Boolean!) {root {objects(all:64,where:{enabled:{eq:$enabled}}) {entries {node {_camino {ref}}}}}}",
|
||||||
])
|
])
|
||||||
await assert.rejects(check(document), /registry|Controller/);
|
await assert.rejects(check(document), /registry|Controller/);
|
||||||
assert.throws(() => parseControllerCatalog(metadata, [iface], []), /registry/);
|
assert.throws(() => parseControllerCatalog(metadata, [iface], []), /registry/);
|
||||||
|
|||||||
@@ -115,10 +115,10 @@ package Owner id "package:owner" revision "package:owner@1" {
|
|||||||
async (name) =>
|
async (name) =>
|
||||||
({
|
({
|
||||||
"inputs.graphql":
|
"inputs.graphql":
|
||||||
"query Inputs($label: String!) { root { objects(all: 100, where: {label: {eq: $label}}) { entries { key node { _qx {ref} label } } } } }",
|
"query Inputs($label: String!) { root { objects(all: 100, where: {label: {eq: $label}}) { entries { key node { _camino {ref} label } } } } }",
|
||||||
"view.graphql":
|
"view.graphql":
|
||||||
"query View { root { availableObjects(all: 100) { entries { key node { _qx {ref} label } } } } }",
|
"query View { root { availableObjects(all: 100) { entries { key node { _camino {ref} label } } } } }",
|
||||||
"editor.graphql": "query Editor { root { _qx {ref} label body notes } }",
|
"editor.graphql": "query Editor { root { _camino {ref} label body notes } }",
|
||||||
"editor-view.graphql": "query EditorView { root { label body notes } }",
|
"editor-view.graphql": "query EditorView { root { label body notes } }",
|
||||||
})[name]!,
|
})[name]!,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ test("a count-only local query still requires complete relationship admission",
|
|||||||
const workspace = await loadReaderWorkspace((name, text) => {
|
const workspace = await loadReaderWorkspace((name, text) => {
|
||||||
if (name === "queries/replica-inputs.graphql") return source;
|
if (name === "queries/replica-inputs.graphql") return source;
|
||||||
if (name === "queries/reader-screen.graphql")
|
if (name === "queries/reader-screen.graphql")
|
||||||
return "query ReaderScreen { root { _qx { relations { highlights { aggregate { count } } } } } }";
|
return "query ReaderScreen { root { _camino { relations { highlights { aggregate { count } } } } } }";
|
||||||
return text;
|
return text;
|
||||||
});
|
});
|
||||||
assert.throws(() => compileExecutionPlan(workspace), /REPLICA_TRAVERSAL_MISSING|REPLICA_RELATION_INCOMPLETE/);
|
assert.throws(() => compileExecutionPlan(workspace), /REPLICA_TRAVERSAL_MISSING|REPLICA_RELATION_INCOMPLETE/);
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
query Enabled {root {objects(all:64, where:{enabled:{eq:true}}) {entries {node {_qx {ref}}}}}}
|
query Enabled {root {objects(all:64, where:{enabled:{eq:true}}) {entries {node {_camino {ref}}}}}}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
query HighlightEditorInputs { root { _qx {ref} color range {sourceRevision start end} } }
|
query HighlightEditorInputs { root { _camino {ref} color range {sourceRevision start end} } }
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
query HighlightEditorView { root { _qx {ref} color @live range {sourceRevision start end} } }
|
query HighlightEditorView { root { _camino {ref} color range {sourceRevision start end} } }
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
query HighlightList($color: String!) { root { availableObjects(all: 200, where: {color: {eq: $color}}, orderBy: {range: {start: ASC}}) { entries { key node { _qx {ref} color @live range {sourceRevision start end} } } } } }
|
query HighlightList($color: String!) { root { availableObjects(all: 200, where: {color: {eq: $color}}, orderBy: {range: {start: ASC}}) { entries { key node { _camino {ref} color range {sourceRevision start end} } } } } }
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
query HighlightScope($color: String!) { root { objects(all: 200, where: {color: {eq: $color}}) { entries { key node { _qx {ref} color range {sourceRevision start end} } } } } }
|
query HighlightScope($color: String!) { root { objects(all: 200, where: {color: {eq: $color}}) { entries { key node { _camino {ref} color range {sourceRevision start end} } } } } }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
query HighlightWindow($color: String!) {
|
query HighlightWindow($color: String!) {
|
||||||
root {
|
root {
|
||||||
objects(first: 2, where: {color: {eq: $color}}, orderBy: {range: {start: ASC}}) {
|
objects(first: 2, where: {color: {eq: $color}}, orderBy: {range: {start: ASC}}) {
|
||||||
entries { key node { _qx {ref} color range {sourceRevision start end} } }
|
entries { key node { _camino {ref} color range {sourceRevision start end} } }
|
||||||
pageInfo { hasNextPage endCursor }
|
pageInfo { hasNextPage endCursor }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ query ReaderScreen($color: String!) {
|
|||||||
entries {
|
entries {
|
||||||
key
|
key
|
||||||
node {
|
node {
|
||||||
_qx { ref }
|
_camino { ref }
|
||||||
range { sourceRevision start end }
|
range { sourceRevision start end }
|
||||||
color @live
|
color
|
||||||
excerpt
|
excerpt
|
||||||
comments(all: 200) {
|
comments(all: 200) {
|
||||||
entries { key node { _qx { ref } content @live { text tags } } }
|
entries { key node { _camino { ref } content { text tags } } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_qx { relations { highlights { aggregate { count } } } }
|
_camino { relations { highlights { aggregate { count } } } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ query ReplicaInputs {
|
|||||||
entries {
|
entries {
|
||||||
key
|
key
|
||||||
node {
|
node {
|
||||||
_qx { ref }
|
_camino { ref }
|
||||||
range { sourceRevision start end }
|
range { sourceRevision start end }
|
||||||
color
|
color
|
||||||
excerpt
|
excerpt
|
||||||
comments(all: 200) {
|
comments(all: 200) {
|
||||||
entries { key node { _qx { ref } content { text tags } } }
|
entries { key node { _camino { ref } content { text tags } } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -67,5 +67,5 @@ export const compileAggregation = (body: string, variables = "") =>
|
|||||||
compileQuery(
|
compileQuery(
|
||||||
declaration,
|
declaration,
|
||||||
interfaces,
|
interfaces,
|
||||||
async () => `query Report${variables} { root { _qx { relations { tasks { ${body} } } } } }`,
|
async () => `query Report${variables} { root { _camino { relations { tasks { ${body} } } } } }`,
|
||||||
);
|
);
|
||||||
|
|||||||
Vendored
+7
-10
@@ -76,22 +76,19 @@ export async function queryWorkspaceFixture(options: { live?: boolean } = {}) {
|
|||||||
const pkg: PackageRevision = pkgResult.resource.revision;
|
const pkg: PackageRevision = pkgResult.resource.revision;
|
||||||
const allInterfaces = [...interfaces.values(), ...(pkgResult.resource.specializations ?? [])];
|
const allInterfaces = [...interfaces.values(), ...(pkgResult.resource.specializations ?? [])];
|
||||||
const documents: Record<string, string> = {
|
const documents: Record<string, string> = {
|
||||||
"extent.graphql": `query Extent($text: String! = "") {root {objects(first: 30, where: {title: {containsInsensitive: $text}}, orderBy: [{rank: ASC}]) {entries {node {_qx {ref} title rank}}}}}`,
|
"extent.graphql": `query Extent($text: String! = "") {root {objects(first: 30, where: {title: {containsInsensitive: $text}}, orderBy: [{rank: ASC}]) {entries {node {_camino {ref} title rank}}}}}`,
|
||||||
"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}}}}`,
|
"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 {_camino {ref} ...TaskRow}} pageInfo {hasNextPage endCursor}}}}`,
|
||||||
"row.graphql": `fragment TaskRow on TaskFacts {title rank done assignee {name}}`,
|
"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}}}}}`,
|
"enriched.graphql": `query Enriched {root {items(first: 3) {entries {key node {_camino {ref} title score}}}}}`,
|
||||||
"ranked.graphql": `query Ranked {root {items(first: 3, where: {score: {gt: 0}}, orderBy: [{score: DESC}]) {entries {key node {_qx {ref} title}}}}}`,
|
"ranked.graphql": `query Ranked {root {items(first: 3, where: {score: {gt: 0}}, orderBy: [{score: DESC}]) {entries {key node {_camino {ref} title}}}}}`,
|
||||||
"totals.graphql": `query Totals {root {_qx {relations {items {aggregate {count sum {rank}} groups(by: {done: true}, first: 10) {entries {group {done} aggregate {count sum {rank}}}}}}}}}`,
|
"totals.graphql": `query Totals {root {_camino {relations {items {aggregate {count sum {rank}} groups(by: {done: true}, first: 10) {entries {group {done} aggregate {count sum {rank}}}}}}}}}`,
|
||||||
"score-totals.graphql": `query ScoreTotals {root {_qx {relations {items {aggregate {count sum {score}}}}}}}`,
|
"score-totals.graphql": `query ScoreTotals {root {_camino {relations {items {aggregate {count sum {score}}}}}}}`,
|
||||||
};
|
};
|
||||||
pkg.checkedQueries = await Promise.all(
|
pkg.checkedQueries = await Promise.all(
|
||||||
pkg.queries!.map((query) =>
|
pkg.queries!.map((query) =>
|
||||||
compileQuery(query, allInterfaces, async (name) =>
|
compileQuery(query, allInterfaces, async (name) =>
|
||||||
options.live
|
options.live
|
||||||
? documents[name]!.replace(/\btitle\b/g, "title @live").replace(
|
? documents[name]!.replace(/\btitle\b/g, "title").replace("title rank done", "title rank done")
|
||||||
"title @live rank done",
|
|
||||||
"title @live rank @live done @live",
|
|
||||||
)
|
|
||||||
: documents[name]!,
|
: documents[name]!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ test("bind chains forward native reads, writes and watches without changing stor
|
|||||||
await compileQuery(
|
await compileQuery(
|
||||||
revision.queries![0]!,
|
revision.queries![0]!,
|
||||||
[...interfaces.values()],
|
[...interfaces.values()],
|
||||||
async () => `query Rows {root {objects(first: 10) {entries {node {title @live}}}}}`,
|
async () => `query Rows {root {objects(first: 10) {entries {node {title}}}}}`,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
result.workspace.packageImports.push(revision);
|
result.workspace.packageImports.push(revision);
|
||||||
|
|||||||
@@ -107,15 +107,10 @@ test("server projection descriptors are distinct from canonical inputs and have
|
|||||||
assert.match(projections, /ConfirmedColor/);
|
assert.match(projections, /ConfirmedColor/);
|
||||||
assert.match(projections, /satisfies ServerProjection/);
|
assert.match(projections, /satisfies ServerProjection/);
|
||||||
assert.match(source.slice(source.indexOf("export const results")), /ConfirmedColor/);
|
assert.match(source.slice(source.indexOf("export const results")), /ConfirmedColor/);
|
||||||
const live = compileExecutionPlan(
|
await assert.rejects(
|
||||||
await loadReaderWorkspace((name, text) =>
|
loadReaderWorkspace((name, text) =>
|
||||||
name === "queries/confirmed-color.graphql" ? text.replace("color", "color @live") : text,
|
name === "queries/confirmed-color.graphql" ? text.replace("color", "color @live") : text,
|
||||||
),
|
),
|
||||||
|
/@live is retired/,
|
||||||
);
|
);
|
||||||
const rejected = generatePortableClientBindings(live, { target: "browser" });
|
|
||||||
assert.doesNotMatch(
|
|
||||||
rejected.slice(rejected.indexOf("export const serverProjections"), rejected.indexOf("export const results")),
|
|
||||||
/ConfirmedColor/,
|
|
||||||
);
|
|
||||||
assert.match(rejected, /QUERY_NOT_SNAPSHOT_PROJECTION/);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ test("aggregate output uses exact count and sum types and nullable reductions",
|
|||||||
|
|
||||||
test("explicit expansion and identity distinct retain checked grouped operands", async () => {
|
test("explicit expansion and identity distinct retain checked grouped operands", async () => {
|
||||||
const checked = await compile(`expand { tags {
|
const checked = await compile(`expand { tags {
|
||||||
distinct(by: {source: {_qx: {ref: true}}, target: {color: true}}) {
|
distinct(by: {source: {_camino: {ref: true}}, target: {color: true}}) {
|
||||||
groups(by: {target: {color: true}}, first: 50,
|
groups(by: {target: {color: true}}, first: 50,
|
||||||
having: {sum: {source: {estimatedHours: {gt: 0}}}},
|
having: {sum: {source: {estimatedHours: {gt: 0}}}},
|
||||||
orderBy: [{sum: {source: {estimatedHours: DESC}}}]) {
|
orderBy: [{sum: {source: {estimatedHours: DESC}}}]) {
|
||||||
@@ -55,7 +55,7 @@ test("distinct and grouping reject fields not determined by their selected keys"
|
|||||||
|
|
||||||
test("relationship existence predicates compile to typed plans", async () => {
|
test("relationship existence predicates compile to typed plans", async () => {
|
||||||
const checked = await compile(
|
const checked = await compile(
|
||||||
`filter(where: {_qx: {relations: {tags: {some: {color: {in: ["red","blue"]}}}}}}) {aggregate {count}}`,
|
`filter(where: {_camino: {relations: {tags: {some: {color: {in: ["red","blue"]}}}}}}) {aggregate {count}}`,
|
||||||
);
|
);
|
||||||
const plan = findPlan(checked.selection)!;
|
const plan = findPlan(checked.selection)!;
|
||||||
assert.equal(plan.stages[1]!.operation.kind, "filter");
|
assert.equal(plan.stages[1]!.operation.kind, "filter");
|
||||||
@@ -88,19 +88,19 @@ test("RPC continuation rejection is local to the affected membership boundary",
|
|||||||
);
|
);
|
||||||
const checked = await compileAggregationDocument(`query Report($after: Cursor) {root {
|
const checked = await compileAggregationDocument(`query Report($after: Cursor) {root {
|
||||||
tasks(first: 2, after: $after) {entries {node {cost}}}
|
tasks(first: 2, after: $after) {entries {node {cost}}}
|
||||||
_qx {relations {tasks {aggregate {sum {score}}}}}
|
_camino {relations {tasks {aggregate {sum {score}}}}}
|
||||||
}}`);
|
}}`);
|
||||||
assert.equal(findPlan(checked.selection)!.terminals[0]!.residual, true);
|
assert.equal(findPlan(checked.selection)!.terminals[0]!.residual, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("typed reference operands and scalar membership tests reject unbounded literal lists", async () => {
|
test("typed reference operands and scalar membership tests reject unbounded literal lists", async () => {
|
||||||
const checked = await compile(
|
const checked = await compile(
|
||||||
`filter(where: {_qx: {ref: {in: $tasks}}}) {aggregate {count}}`,
|
`filter(where: {_camino: {ref: {in: $tasks}}}) {aggregate {count}}`,
|
||||||
"($tasks: [QxRef_Task!]!)",
|
"($tasks: [QxRef_Task!]!)",
|
||||||
);
|
);
|
||||||
assert.match(JSON.stringify(checked.variables), /object-ref/);
|
assert.match(JSON.stringify(checked.variables), /object-ref/);
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
compile(`filter(where: {_qx: {ref: {eq: "obj:made-up"}}}) {aggregate {count}}`),
|
compile(`filter(where: {_camino: {ref: {eq: "obj:made-up"}}}) {aggregate {count}}`),
|
||||||
/typed variables/,
|
/typed variables/,
|
||||||
);
|
);
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
|
|||||||
+14
-32
@@ -4,7 +4,6 @@ import { compileCapabilityResourceSource } from "../src/capability-language/inde
|
|||||||
import { compileQuery } from "../src/query/compile.js";
|
import { compileQuery } from "../src/query/compile.js";
|
||||||
import { QueryCompileError } from "../src/query/types.js";
|
import { QueryCompileError } from "../src/query/types.js";
|
||||||
import { checkQueryTemplate } from "../src/query/templates.js";
|
import { checkQueryTemplate } from "../src/query/templates.js";
|
||||||
import { queryPresentation } from "../src/query/presentation.js";
|
|
||||||
import { querySelectionToWire } from "../src/query/proto.js";
|
import { querySelectionToWire } from "../src/query/proto.js";
|
||||||
import type { InterfaceRevision } from "../src/capability-model/types.js";
|
import type { InterfaceRevision } from "../src/capability-model/types.js";
|
||||||
|
|
||||||
@@ -149,9 +148,9 @@ test("workspace roots expose a bounded interface extent, not a root object or wr
|
|||||||
async () => `query Upcoming {
|
async () => `query Upcoming {
|
||||||
root {
|
root {
|
||||||
objects(first: 3, where: {done: {eq: false}}, orderBy: {title: ASC}) {
|
objects(first: 3, where: {done: {eq: false}}, orderBy: {title: ASC}) {
|
||||||
entries {key node {_qx {ref} title @live}} pageInfo {hasNextPage endCursor}
|
entries {key node {_camino {ref} title}} pageInfo {hasNextPage endCursor}
|
||||||
}
|
}
|
||||||
_qx {relations {objects {aggregate {count sum {due}}}}}
|
_camino {relations {objects {aggregate {count sum {due}}}}}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
@@ -159,7 +158,7 @@ test("workspace roots expose a bounded interface extent, not a root object or wr
|
|||||||
assert.ok(!checked.effects.some((effect) => effect.memberId.includes("workspace-objects")));
|
assert.ok(!checked.effects.some((effect) => effect.memberId.includes("workspace-objects")));
|
||||||
assert.ok(JSON.stringify(checked.selection).includes("qx-internal-workspace-objects"));
|
assert.ok(JSON.stringify(checked.selection).includes("qx-internal-workspace-objects"));
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
compileQuery(declaration, [facts], async () => `query Upcoming {root {_qx {ref}}}`),
|
compileQuery(declaration, [facts], async () => `query Upcoming {root {_camino {ref}}}`),
|
||||||
/QUERY_VALIDATION/,
|
/QUERY_VALIDATION/,
|
||||||
);
|
);
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
@@ -169,51 +168,34 @@ test("workspace roots expose a bounded interface extent, not a root object or wr
|
|||||||
});
|
});
|
||||||
const document = `query Upcoming($first: Int!, $before: Int64!) {
|
const document = `query Upcoming($first: Int!, $before: Int64!) {
|
||||||
root { items(first: $first, where: {done: {eq: false}, due: {lt: $before}}, orderBy: [{due: ASC}]) {
|
root { items(first: $first, where: {done: {eq: false}, due: {lt: $before}}, orderBy: [{due: ASC}]) {
|
||||||
entries { key node { _qx { ref } ...Row } } pageInfo { hasNextPage endCursor }
|
entries { key node { _camino { ref } ...Row } } pageInfo { hasNextPage endCursor }
|
||||||
} }
|
} }
|
||||||
}`;
|
}`;
|
||||||
const compile = (query = document, row = "fragment Row on TaskFacts { title due }", clauses = "") =>
|
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));
|
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 () => {
|
test("ordinary selections preserve aliases, fragment conditions and wire identity", async () => {
|
||||||
const checked = await compile(
|
const checked = await compile(
|
||||||
`query Upcoming($show: Boolean!) {root {items(first: 3) {entries {node {plain: title ...Row @include(if: $show)}}}}}`,
|
`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}`,
|
`fragment Row on TaskFacts {label: title due}`,
|
||||||
);
|
);
|
||||||
const fields = queryPresentation(checked, [collection, facts]);
|
const node = checked.selection[0]!.selection[0]!.selection[0]!.selection[0]!;
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
fields.map((f) => f.path),
|
node.selection.map((s) => s.key),
|
||||||
[
|
["plain", "label", "due"],
|
||||||
["root", "items", "entries", "*", "node", "label"],
|
|
||||||
["root", "items", "entries", "*", "node", "due"],
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
assert.equal(fields[0]!.getOperationId, "title:get");
|
assert.equal(node.selection[1]!.conditions.length, 1);
|
||||||
assert.equal(fields[0]!.setOperationId, undefined);
|
assert.ok(JSON.stringify(checked.selection.map(querySelectionToWire)).includes("label"));
|
||||||
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("retired live syntax fails with migration guidance", async () => {
|
||||||
test("live selections reject RPC getters, synthetic selections and conflicting presentations", async () => {
|
for (const selected of ["score @live", "_camino @live {ref}", "title @live title", "title @live(unchecked: true)"])
|
||||||
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(
|
await assert.rejects(
|
||||||
compile(
|
compile(
|
||||||
`query Upcoming {root {items(first: 3) {entries {node {...Row}}}}}`,
|
`query Upcoming {root {items(first: 3) {entries {node {...Row}}}}}`,
|
||||||
`fragment Row on TaskFacts {${selected}}`,
|
`fragment Row on TaskFacts {${selected}}`,
|
||||||
'allow TaskFacts.score select "bounded";',
|
'allow TaskFacts.score select "bounded";',
|
||||||
),
|
),
|
||||||
(error: unknown) => error instanceof QueryCompileError && error.code === code,
|
(error: unknown) => error instanceof QueryCompileError && error.code === "QUERY_RETIRED_DIRECTIVE",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ test("reference-only workspace queries hash every eligible conformance, includin
|
|||||||
const checked = await compileQuery(
|
const checked = await compileQuery(
|
||||||
query,
|
query,
|
||||||
workspace.interfaceImports,
|
workspace.interfaceImports,
|
||||||
async () => `query ${query.operation} {root {objects(first: 10) {entries {node {_qx {ref}}}}}}`,
|
async () => `query ${query.operation} {root {objects(first: 10) {entries {node {_camino {ref}}}}}}`,
|
||||||
);
|
);
|
||||||
pkg.checkedQueries = pkg.checkedQueries!.map((q) => (q.declaration.id === query.id ? checked : q));
|
pkg.checkedQueries = pkg.checkedQueries!.map((q) => (q.declaration.id === query.id ? checked : q));
|
||||||
const linked = linkQueries(workspace).find((q) => q.id.endsWith(":upcoming"))!;
|
const linked = linkQueries(workspace).find((q) => q.id.endsWith(":upcoming"))!;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ test("React bindings preserve read-only, writable and nested reference contracts
|
|||||||
await compileQuery(
|
await compileQuery(
|
||||||
pkg.resource.revision.queries![0]!,
|
pkg.resource.revision.queries![0]!,
|
||||||
[iface.resource.revision],
|
[iface.resource.revision],
|
||||||
async () => "query Editor {root {title @live summary @live plain: title}}",
|
async () => "query Editor {root {title summary plain: title}}",
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
const schema = {
|
const schema = {
|
||||||
@@ -79,16 +79,10 @@ test("React bindings preserve read-only, writable and nested reference contracts
|
|||||||
import {reactInterfaces} from "./react-props.gen.js";
|
import {reactInterfaces} from "./react-props.gen.js";
|
||||||
import type {ReactResults, QueryResults} from "./react-props.gen.js";
|
import type {ReactResults, QueryResults} from "./react-props.gen.js";
|
||||||
declare const query: QueryResults["Editor"];
|
declare const query: QueryResults["Editor"];
|
||||||
useLiveField(query.root.title).set("new");
|
const queryTitle: string = query.root.title;
|
||||||
useLiveField(detachField(query.root.title)).set("new");
|
const querySummary: string = query.root.summary;
|
||||||
// @ts-expect-error detachment preserves readonly access
|
// @ts-expect-error query values are not field mutation handles
|
||||||
useLiveField(detachField(query.root.summary)).set("no");
|
query.root.title.set("new");
|
||||||
// @ts-expect-error query handles do not expose transport metadata
|
|
||||||
query.root.title.capability;
|
|
||||||
// @ts-expect-error live query fields retain exact setter types
|
|
||||||
useLiveField(query.root.title).set(123);
|
|
||||||
// @ts-expect-error readonly query fields do not acquire a setter
|
|
||||||
useLiveField(query.root.summary).set("no");
|
|
||||||
const plain: string = query.root.plain;
|
const plain: string = query.root.plain;
|
||||||
async function lookup() {
|
async function lookup() {
|
||||||
const view = await tryConform("object", reactInterfaces.Fields);
|
const view = await tryConform("object", reactInterfaces.Fields);
|
||||||
|
|||||||
Reference in New Issue
Block a user