diff --git a/src/bindings/portable-client.ts b/src/bindings/portable-client.ts index 1f9ac00..7ba014b 100644 --- a/src/bindings/portable-client.ts +++ b/src/bindings/portable-client.ts @@ -76,13 +76,10 @@ export function generatePortableClientBindings( const operations = query.operations.map( (key) => plan.definition.operations.find((operation) => operation.key === key)!, ); - const live = (selections: readonly QuerySelection[]): boolean => - selections.some((selection) => !!selection.liveSelectionId || live(selection.selection)); if ( operations.some( (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" }); continue; diff --git a/src/bindings/react.ts b/src/bindings/react.ts index 237caa8..5034cd6 100644 --- a/src/bindings/react.ts +++ b/src/bindings/react.ts @@ -1,6 +1,5 @@ import type { BindingSchema } from "./index.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. */ export function generateReactBindings( @@ -131,25 +130,9 @@ export function generateReactBindings( return type(value); }; const queries = pkg.checkedQueries ?? []; - const presentedType = ( - value: ValueType, - fields: ReturnType, - 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 = `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`).join(";")}} = ${JSON.stringify( Object.fromEntries( queries.map((query) => [ @@ -160,7 +143,6 @@ export function generateReactBindings( root: query.declaration.root, variables: query.variables, output: query.output, - liveFields: queryPresentation(query, schema.interfaces), watch: query.declaration.watch, }, ]), diff --git a/src/capability-model/controllers.ts b/src/capability-model/controllers.ts index 6fbc942..b06acd9 100644 --- a/src/capability-model/controllers.ts +++ b/src/capability-model/controllers.ts @@ -24,7 +24,7 @@ export function validateControllers( objects = selected(root?.selection, "objects"), entries = selected(objects?.selection, "entries"), node = selected(entries?.selection, "node"), - qx = selected(node?.selection, "_qx"), + qx = selected(node?.selection, "_camino"), ref = selected(qx?.selection, "ref"); const all = objects?.arguments.all; if ( @@ -44,7 +44,7 @@ export function validateControllers( ) fail( `${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); diff --git a/src/query/compile.ts b/src/query/compile.ts index 4fd4ee9..e1d4be7 100644 --- a/src/query/compile.ts +++ b/src/query/compile.ts @@ -87,6 +87,16 @@ export async function compileQuery( definitions.push(...document.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 operations = definitions.filter((entry) => entry.kind === "OperationDefinition"); if ( @@ -201,16 +211,7 @@ export async function compileQuery( Field(node) { const parent = info.getParentType(); const contract = parent && generated.byName.get(parent.name); - if (node.directives?.some((directive) => directive.name.value === "live")) { - 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; + if (!contract || node.name.value === "_camino") return; mark(contract.revisionId, node.name.value, "select", node); const member = contract.members.find((entry) => entry.displayName === node.name.value)!; if (member.kind !== "relationship" || (member.cardinality !== "many" && member.cardinality !== "many-unique")) @@ -425,13 +426,6 @@ export async function compileQuery( { name: 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?.kind === "relationship" ? { targetInterfaceRevisionId: generated.target(member) } : {}), conditions: [...inherited, ...conditions(node.directives)], @@ -447,18 +441,6 @@ export async function compileQuery( const normalized = print(document), schema = printSchema(generated.schema); 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") .update( canonicalJson({ diff --git a/src/query/presentation.ts b/src/query/presentation.ts deleted file mode 100644 index 2e9ddb5..0000000 --- a/src/query/presentation.ts +++ /dev/null @@ -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; -} diff --git a/src/query/relational-compile.ts b/src/query/relational-compile.ts index c574ad1..db1fb6a 100644 --- a/src/query/relational-compile.ts +++ b/src/query/relational-compile.ts @@ -94,7 +94,8 @@ export function relationalCompiler( if (!prefix(base, path)) return false; // Object identity does not determine which incoming membership carried it. 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) @@ -153,7 +154,7 @@ export function relationalCompiler( available?: Availability, ): QueryExpression => { requireAvailable(names, available, node); - const meta = names.at(-2) === "_qx"; + const meta = names.at(-2) === "_camino"; let fieldIndex = names.length - 1; if (!meta) { let cursor = root; @@ -334,10 +335,10 @@ export function relationalCompiler( children.push({ kind: "not", child: { kind: "and", children: children.splice(before) } }); } else if (current.kind === "pair" && (name === "source" || name === "target")) visit(current[name], field.value, [...base, name]); - else if (name === "_qx") { + else if (name === "_camino") { for (const meta of pairs(field.value)) { 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( ...(comparisons(meta.value, (operator, value) => ({ kind: "compare", @@ -531,7 +532,7 @@ export function relationalCompiler( expression: { ...k.expression, path: [{ kind: "source" as const }, ...k.expression.path] }, })), { - path: ["target", "_qx", "entry"], + path: ["target", "_camino", "entry"], expression: { path: [{ kind: "target" as const }], leaf: { kind: "entry" as const }, diff --git a/src/query/relational-schema.ts b/src/query/relational-schema.ts index ccdff64..04612ba 100644 --- a/src/query/relational-schema.ts +++ b/src/query/relational-schema.ts @@ -205,7 +205,9 @@ export function relationalSchema(env: RelationalSchemaEnvironment, document?: Do metadata[name] = { type: !operator && nullable ? t : new GraphQLNonNull(t) }; } 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 // keeps it valid while compiler validation forbids selecting that sentinel. 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 }; } 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; }); const comparisons = ( @@ -266,7 +268,7 @@ export function relationalSchema(env: RelationalSchemaEnvironment, document?: Do if (!operator || operator === "countPresent") meta[name] = { type: ordering ? env.direction : compare(operator ? aggregateType(operator, type) : type) }; 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 }; return result; }, @@ -306,7 +308,7 @@ export function relationalSchema(env: RelationalSchemaEnvironment, document?: Do if (field.kind === "leaf" && !comparableValue(field.type)) continue; result[name] = { type: field.kind === "row" ? where(field.row) : compare(field.type) }; } - result._qx = { + result._camino = { type: inp("WhereMeta", row, () => { const meta: GraphQLInputFieldConfigMap = {}; 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) => { for (const node of selections(set)) if (node.selectionSet) { - if (node.name.value === "_qx") { + if (node.name.value === "_camino") { for (const meta of selections(node.selectionSet)) if (meta.name.value === "relations" && meta.selectionSet) for (const edge of selections(meta.selectionSet)) { diff --git a/src/query/schema.ts b/src/query/schema.ts index 45403f5..63a0cc5 100644 --- a/src/query/schema.ts +++ b/src/query/schema.ts @@ -11,8 +11,6 @@ import { GraphQLList, GraphQLNonNull, GraphQLSchema, - GraphQLDirective, - DirectiveLocation, specifiedDirectives, type GraphQLOutputType, type GraphQLInputType, @@ -198,7 +196,7 @@ export function querySchema( ...(helpers ? { relations: { type: new GraphQLNonNull(helpers) } } : {}), }, }); - const fields: GraphQLFieldConfigMap = { _qx: { type: new GraphQLNonNull(metadata) } }; + const fields: GraphQLFieldConfigMap = { _camino: { type: new GraphQLNonNull(metadata) } }; for (const member of contract(id).members) { if (member.kind === "operation" || member.kind === "module" || !member.queryRead) continue; if (!/^[_A-Za-z][_0-9A-Za-z]*$/.test(member.displayName) || member.displayName.startsWith("_")) @@ -258,7 +256,7 @@ export function querySchema( return result; }; const schema = new GraphQLSchema({ - directives: [...specifiedDirectives, new GraphQLDirective({ name: "live", locations: [DirectiveLocation.FIELD] })], + directives: specifiedDirectives, query: new GraphQLObjectType({ name: "QxQuery", fields: { root: { type: new GraphQLNonNull(object(root.interface)) } }, diff --git a/src/query/types.ts b/src/query/types.ts index 4c6eff3..bffbe61 100644 --- a/src/query/types.ts +++ b/src/query/types.ts @@ -102,7 +102,6 @@ export type QueryArgument = | { kind: "list"; values: QueryArgument[] } | { kind: "object"; fields: Record }; export interface QuerySelection { - liveSelectionId?: string; name: string; key: string; interfaceRevisionId?: InterfaceRevisionId; diff --git a/test/controller-registry.test.ts b/test/controller-registry.test.ts index a22ea98..177b0af 100644 --- a/test/controller-registry.test.ts +++ b/test/controller-registry.test.ts @@ -34,16 +34,16 @@ test("controller discovery requires complete bounded accepted-state membership", const check = async (document: string) => parseControllerCatalog(metadata, [iface], [await compileQuery(declaration, [iface], async () => document)]); 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, 1, ); for (const document of [ - "query Enabled {root {objects(first:64) {entries {node {_qx {ref}}}}}}", - "query Enabled {root {objects(all:65) {entries {node {_qx {ref}}}}}}", - "query Enabled {root {devices:objects(all:64) {entries {node {_qx {ref}}}}}}", + "query Enabled {root {objects(first:64) {entries {node {_camino {ref}}}}}}", + "query Enabled {root {objects(all:65) {entries {node {_camino {ref}}}}}}", + "query Enabled {root {devices:objects(all:64) {entries {node {_camino {ref}}}}}}", "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/); assert.throws(() => parseControllerCatalog(metadata, [iface], []), /registry/); diff --git a/test/emit-managed.ts b/test/emit-managed.ts index 59c39a7..cbb255f 100644 --- a/test/emit-managed.ts +++ b/test/emit-managed.ts @@ -115,10 +115,10 @@ package Owner id "package:owner" revision "package:owner@1" { async (name) => ({ "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": - "query View { root { availableObjects(all: 100) { entries { key node { _qx {ref} label } } } } }", - "editor.graphql": "query Editor { root { _qx {ref} label body notes } }", + "query View { root { availableObjects(all: 100) { entries { key node { _camino {ref} label } } } } }", + "editor.graphql": "query Editor { root { _camino {ref} label body notes } }", "editor-view.graphql": "query EditorView { root { label body notes } }", })[name]!, ), diff --git a/test/execution-world.test.ts b/test/execution-world.test.ts index af3704e..963d99f 100644 --- a/test/execution-world.test.ts +++ b/test/execution-world.test.ts @@ -302,7 +302,7 @@ test("a count-only local query still requires complete relationship admission", const workspace = await loadReaderWorkspace((name, text) => { if (name === "queries/replica-inputs.graphql") return source; 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; }); assert.throws(() => compileExecutionPlan(workspace), /REPLICA_TRAVERSAL_MISSING|REPLICA_RELATION_INCOMPLETE/); diff --git a/test/fixtures/controller-device/enabled.graphql b/test/fixtures/controller-device/enabled.graphql index 64b56c9..5911764 100644 --- a/test/fixtures/controller-device/enabled.graphql +++ b/test/fixtures/controller-device/enabled.graphql @@ -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}}}}}} diff --git a/test/fixtures/local-first-reader/queries/editor-inputs.graphql b/test/fixtures/local-first-reader/queries/editor-inputs.graphql index b69323d..d506888 100644 --- a/test/fixtures/local-first-reader/queries/editor-inputs.graphql +++ b/test/fixtures/local-first-reader/queries/editor-inputs.graphql @@ -1 +1 @@ -query HighlightEditorInputs { root { _qx {ref} color range {sourceRevision start end} } } +query HighlightEditorInputs { root { _camino {ref} color range {sourceRevision start end} } } diff --git a/test/fixtures/local-first-reader/queries/editor-view.graphql b/test/fixtures/local-first-reader/queries/editor-view.graphql index 97a1cc4..f205319 100644 --- a/test/fixtures/local-first-reader/queries/editor-view.graphql +++ b/test/fixtures/local-first-reader/queries/editor-view.graphql @@ -1 +1 @@ -query HighlightEditorView { root { _qx {ref} color @live range {sourceRevision start end} } } +query HighlightEditorView { root { _camino {ref} color range {sourceRevision start end} } } diff --git a/test/fixtures/local-first-reader/queries/highlight-list.graphql b/test/fixtures/local-first-reader/queries/highlight-list.graphql index 13e0094..3fb23b7 100644 --- a/test/fixtures/local-first-reader/queries/highlight-list.graphql +++ b/test/fixtures/local-first-reader/queries/highlight-list.graphql @@ -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} } } } } } diff --git a/test/fixtures/local-first-reader/queries/highlight-scope.graphql b/test/fixtures/local-first-reader/queries/highlight-scope.graphql index eb35067..db46a43 100644 --- a/test/fixtures/local-first-reader/queries/highlight-scope.graphql +++ b/test/fixtures/local-first-reader/queries/highlight-scope.graphql @@ -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} } } } } } diff --git a/test/fixtures/local-first-reader/queries/highlight-window.graphql b/test/fixtures/local-first-reader/queries/highlight-window.graphql index b19d729..3fc3d1c 100644 --- a/test/fixtures/local-first-reader/queries/highlight-window.graphql +++ b/test/fixtures/local-first-reader/queries/highlight-window.graphql @@ -1,7 +1,7 @@ query HighlightWindow($color: String!) { root { 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 } } } diff --git a/test/fixtures/local-first-reader/queries/reader-screen.graphql b/test/fixtures/local-first-reader/queries/reader-screen.graphql index 1f50d8c..3ed97d2 100644 --- a/test/fixtures/local-first-reader/queries/reader-screen.graphql +++ b/test/fixtures/local-first-reader/queries/reader-screen.graphql @@ -6,16 +6,16 @@ query ReaderScreen($color: String!) { entries { key node { - _qx { ref } + _camino { ref } range { sourceRevision start end } - color @live + color excerpt 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 } } } } } } diff --git a/test/fixtures/local-first-reader/queries/replica-inputs.graphql b/test/fixtures/local-first-reader/queries/replica-inputs.graphql index b69387b..7c8dde8 100644 --- a/test/fixtures/local-first-reader/queries/replica-inputs.graphql +++ b/test/fixtures/local-first-reader/queries/replica-inputs.graphql @@ -6,12 +6,12 @@ query ReplicaInputs { entries { key node { - _qx { ref } + _camino { ref } range { sourceRevision start end } color excerpt comments(all: 200) { - entries { key node { _qx { ref } content { text tags } } } + entries { key node { _camino { ref } content { text tags } } } } } } diff --git a/test/fixtures/query-aggregation.ts b/test/fixtures/query-aggregation.ts index 10c30e9..b107ebd 100644 --- a/test/fixtures/query-aggregation.ts +++ b/test/fixtures/query-aggregation.ts @@ -67,5 +67,5 @@ export const compileAggregation = (body: string, variables = "") => compileQuery( declaration, interfaces, - async () => `query Report${variables} { root { _qx { relations { tasks { ${body} } } } } }`, + async () => `query Report${variables} { root { _camino { relations { tasks { ${body} } } } } }`, ); diff --git a/test/fixtures/query-workspace.ts b/test/fixtures/query-workspace.ts index d2d7071..9999d65 100644 --- a/test/fixtures/query-workspace.ts +++ b/test/fixtures/query-workspace.ts @@ -76,22 +76,19 @@ export async function queryWorkspaceFixture(options: { live?: boolean } = {}) { const pkg: PackageRevision = pkgResult.resource.revision; const allInterfaces = [...interfaces.values(), ...(pkgResult.resource.specializations ?? [])]; const documents: Record = { - "extent.graphql": `query Extent($text: String! = "") {root {objects(first: 30, where: {title: {containsInsensitive: $text}}, orderBy: [{rank: ASC}]) {entries {node {_qx {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}}}}`, + "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 {_camino {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}}}}}`, - "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}}}}}}}}}`, - "score-totals.graphql": `query ScoreTotals {root {_qx {relations {items {aggregate {count sum {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 {_camino {ref} title}}}}}`, + "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 {_camino {relations {items {aggregate {count sum {score}}}}}}}`, }; pkg.checkedQueries = await Promise.all( pkg.queries!.map((query) => compileQuery(query, allInterfaces, async (name) => options.live - ? documents[name]!.replace(/\btitle\b/g, "title @live").replace( - "title @live rank done", - "title @live rank @live done @live", - ) + ? documents[name]!.replace(/\btitle\b/g, "title").replace("title rank done", "title rank done") : documents[name]!, ), ), diff --git a/test/forwarding.test.ts b/test/forwarding.test.ts index 487d955..5674429 100644 --- a/test/forwarding.test.ts +++ b/test/forwarding.test.ts @@ -81,7 +81,7 @@ test("bind chains forward native reads, writes and watches without changing stor await compileQuery( revision.queries![0]!, [...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); diff --git a/test/portable-bindings.test.ts b/test/portable-bindings.test.ts index 1f8b458..9e11724 100644 --- a/test/portable-bindings.test.ts +++ b/test/portable-bindings.test.ts @@ -107,15 +107,10 @@ test("server projection descriptors are distinct from canonical inputs and have assert.match(projections, /ConfirmedColor/); assert.match(projections, /satisfies ServerProjection/); assert.match(source.slice(source.indexOf("export const results")), /ConfirmedColor/); - const live = compileExecutionPlan( - await loadReaderWorkspace((name, text) => + await assert.rejects( + loadReaderWorkspace((name, 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/); }); diff --git a/test/query-aggregation.test.ts b/test/query-aggregation.test.ts index 91bb7b7..5a563d8 100644 --- a/test/query-aggregation.test.ts +++ b/test/query-aggregation.test.ts @@ -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 () => { 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, having: {sum: {source: {estimatedHours: {gt: 0}}}}, 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 () => { 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)!; 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 { 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); }); test("typed reference operands and scalar membership tests reject unbounded literal lists", async () => { const checked = await compile( - `filter(where: {_qx: {ref: {in: $tasks}}}) {aggregate {count}}`, + `filter(where: {_camino: {ref: {in: $tasks}}}) {aggregate {count}}`, "($tasks: [QxRef_Task!]!)", ); assert.match(JSON.stringify(checked.variables), /object-ref/); 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/, ); await assert.rejects( diff --git a/test/query-compiler.test.ts b/test/query-compiler.test.ts index 0170666..cbdc52d 100644 --- a/test/query-compiler.test.ts +++ b/test/query-compiler.test.ts @@ -4,7 +4,6 @@ import { compileCapabilityResourceSource } from "../src/capability-language/inde import { compileQuery } from "../src/query/compile.js"; import { QueryCompileError } from "../src/query/types.js"; import { checkQueryTemplate } from "../src/query/templates.js"; -import { queryPresentation } from "../src/query/presentation.js"; import { querySelectionToWire } from "../src/query/proto.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 { root { 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(JSON.stringify(checked.selection).includes("qx-internal-workspace-objects")); await assert.rejects( - compileQuery(declaration, [facts], async () => `query Upcoming {root {_qx {ref}}}`), + compileQuery(declaration, [facts], async () => `query Upcoming {root {_camino {ref}}}`), /QUERY_VALIDATION/, ); 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!) { 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 = "") => 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( `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( - fields.map((f) => f.path), - [ - ["root", "items", "entries", "*", "node", "label"], - ["root", "items", "entries", "*", "node", "due"], - ], + node.selection.map((s) => s.key), + ["plain", "label", "due"], ); - assert.equal(fields[0]!.getOperationId, "title:get"); - assert.equal(fields[0]!.setOperationId, undefined); - 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); + assert.equal(node.selection[1]!.conditions.length, 1); + assert.ok(JSON.stringify(checked.selection.map(querySelectionToWire)).includes("label")); }); - -test("live selections reject RPC getters, synthetic selections and conflicting presentations", async () => { - 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"], - ]) +test("retired live syntax fails with migration guidance", async () => { + for (const selected of ["score @live", "_camino @live {ref}", "title @live title", "title @live(unchecked: true)"]) await assert.rejects( compile( `query Upcoming {root {items(first: 3) {entries {node {...Row}}}}}`, `fragment Row on TaskFacts {${selected}}`, 'allow TaskFacts.score select "bounded";', ), - (error: unknown) => error instanceof QueryCompileError && error.code === code, + (error: unknown) => error instanceof QueryCompileError && error.code === "QUERY_RETIRED_DIRECTIVE", ); }); diff --git a/test/query-workspace.test.ts b/test/query-workspace.test.ts index 0c17d5a..f04df42 100644 --- a/test/query-workspace.test.ts +++ b/test/query-workspace.test.ts @@ -39,7 +39,7 @@ test("reference-only workspace queries hash every eligible conformance, includin const checked = await compileQuery( query, 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)); const linked = linkQueries(workspace).find((q) => q.id.endsWith(":upcoming"))!; diff --git a/test/react-fields.test.ts b/test/react-fields.test.ts index ba39d02..767f733 100644 --- a/test/react-fields.test.ts +++ b/test/react-fields.test.ts @@ -33,7 +33,7 @@ test("React bindings preserve read-only, writable and nested reference contracts await compileQuery( pkg.resource.revision.queries![0]!, [iface.resource.revision], - async () => "query Editor {root {title @live summary @live plain: title}}", + async () => "query Editor {root {title summary plain: title}}", ), ]; 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 type {ReactResults, QueryResults} from "./react-props.gen.js"; declare const query: QueryResults["Editor"]; -useLiveField(query.root.title).set("new"); -useLiveField(detachField(query.root.title)).set("new"); -// @ts-expect-error detachment preserves readonly access -useLiveField(detachField(query.root.summary)).set("no"); -// @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 queryTitle: string = query.root.title; +const querySummary: string = query.root.summary; +// @ts-expect-error query values are not field mutation handles +query.root.title.set("new"); const plain: string = query.root.plain; async function lookup() { const view = await tryConform("object", reactInterfaces.Fields);