Add exported query fragments and scoped coverage observations

This commit is contained in:
Timothy J. Aveni
2026-09-22 16:18:57 -07:00
parent eba85ba158
commit a8869e4545
13 changed files with 2503 additions and 1715 deletions
+8 -2
View File
@@ -163,8 +163,10 @@ presentationDecl
; ;
packageQuery packageQuery
: QUERY identifier typeParameters? ID stringLiteral ROOT WORKSPACE? interfaceType : QUERY FRAGMENT? identifier typeParameters? ID stringLiteral ROOT WORKSPACE? interfaceType
DOCUMENT stringLiteral OPERATION stringLiteral LBRACE queryClause* RBRACE DOCUMENT stringLiteral
(OPERATION stringLiteral LBRACE queryClause* RBRACE
| FRAGMENT stringLiteral (OPERATION stringLiteral)? (LBRACE queryClause* RBRACE | SEMI))
; ;
packageQuerySpecialization packageQuerySpecialization
@@ -175,6 +177,7 @@ queryClause
: REPLICA SEMI : REPLICA SEMI
| LOCAL FROM identifier SEMI | LOCAL FROM identifier SEMI
| FRAGMENTS stringLiteral SEMI | FRAGMENTS stringLiteral SEMI
| FRAGMENTS identifier DOT identifier (WITH LBRACE fragmentVariable* RBRACE)? SEMI
| VIEW OBJECT? identifier AS interfaceType SEMI | VIEW OBJECT? identifier AS interfaceType SEMI
| MAX identifier INTEGER SEMI | MAX identifier INTEGER SEMI
| ALLOW interfaceType DOT identifier identifier stringLiteral SEMI | ALLOW interfaceType DOT identifier identifier stringLiteral SEMI
@@ -182,6 +185,8 @@ queryClause
| POLL INTEGER stringLiteral SEMI | POLL INTEGER stringLiteral SEMI
; ;
fragmentVariable : identifier TO identifier SEMI;
packageOperationExport packageOperationExport
: OPERATION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType : OPERATION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType
MODE operationMode executionClause? eventClause? RECEIVER receiverRequirement dependencyBlock? acceptanceClause? SEMI MODE operationMode executionClause? eventClause? RECEIVER receiverRequirement dependencyBlock? acceptanceClause? SEMI
@@ -482,6 +487,7 @@ identifier
| ROOT | ROOT
| DOCUMENT | DOCUMENT
| FRAGMENTS | FRAGMENTS
| FRAGMENT
| VIEW | VIEW
| MAX | MAX
| ALLOW | ALLOW
+31 -6
View File
@@ -276,12 +276,37 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
await checkQueryTemplate(template, queryInterfaces, (name) => await checkQueryTemplate(template, queryInterfaces, (name) =>
readRepositorySource(snapshot.directory, name), readRepositorySource(snapshot.directory, name),
); );
if (compiled.resource.revision.queries?.length) if (compiled.resource.revision.queries?.length) {
compiled.resource.revision.checkedQueries = await Promise.all( const pkg = compiled.resource.revision;
compiled.resource.revision.queries.map((query) => const checked = new Map<string, import("../query/types.js").CheckedQuery>();
compileRepositoryQuery(snapshot.directory, query, queryInterfaces), const compile = async (
), id: string,
); parents: readonly string[] = [],
): Promise<import("../query/types.js").CheckedQuery> => {
if (parents.includes(id)) throw new Error(`Query fragment cycle: ${[...parents, id].join(" -> ")}`);
const previous = checked.get(id);
if (previous) return previous;
const query = pkg.queries!.find((query) => query.id === id);
if (!query) throw new Error(`Unknown fragment ${pkg.revisionId}/${id}`);
const result = await compileRepositoryQuery(
snapshot.directory,
query,
queryInterfaces,
async (packageId, fragmentId) => {
if (packageId === pkg.revisionId) return compile(fragmentId, [...parents, id]);
const imported = environment.packageClosure
?.find((other) => other.revisionId === packageId)
?.checkedQueries?.find((query) => query.declaration.id === fragmentId);
if (!imported) throw new Error(`Unchecked imported fragment ${packageId}/${fragmentId}`);
return imported;
},
);
checked.set(id, result);
return result;
};
pkg.checkedQueries = [];
for (const query of pkg.queries!) pkg.checkedQueries.push(await compile(query.id));
}
let controllerText: string | undefined; let controllerText: string | undefined;
try { try {
controllerText = await readRepositorySource(snapshot.directory, "quixos.controllers.json"); controllerText = await readRepositorySource(snapshot.directory, "quixos.controllers.json");
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -35,6 +35,7 @@ import { PresentationDeclContext } from "./QuixosCapabilityParser.js";
import { PackageQueryContext } from "./QuixosCapabilityParser.js"; import { PackageQueryContext } from "./QuixosCapabilityParser.js";
import { PackageQuerySpecializationContext } from "./QuixosCapabilityParser.js"; import { PackageQuerySpecializationContext } from "./QuixosCapabilityParser.js";
import { QueryClauseContext } from "./QuixosCapabilityParser.js"; import { QueryClauseContext } from "./QuixosCapabilityParser.js";
import { FragmentVariableContext } from "./QuixosCapabilityParser.js";
import { PackageOperationExportContext } from "./QuixosCapabilityParser.js"; import { PackageOperationExportContext } from "./QuixosCapabilityParser.js";
import { PackageFunctionExportContext } from "./QuixosCapabilityParser.js"; import { PackageFunctionExportContext } from "./QuixosCapabilityParser.js";
import { AcceptanceClauseContext } from "./QuixosCapabilityParser.js"; import { AcceptanceClauseContext } from "./QuixosCapabilityParser.js";
@@ -291,6 +292,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result * @return the visitor result
*/ */
visitQueryClause?: (ctx: QueryClauseContext) => Result; visitQueryClause?: (ctx: QueryClauseContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.fragmentVariable`.
* @param ctx the parse tree
* @return the visitor result
*/
visitFragmentVariable?: (ctx: FragmentVariableContext) => Result;
/** /**
* Visit a parse tree produced by `QuixosCapabilityParser.packageOperationExport`. * Visit a parse tree produced by `QuixosCapabilityParser.packageOperationExport`.
* @param ctx the parse tree * @param ctx the parse tree
+64 -3
View File
@@ -1380,6 +1380,14 @@ const lowerPackage = (
if (exportContext.packageQuerySpecialization()) return []; if (exportContext.packageQuerySpecialization()) return [];
const query = exportContext.packageQuery(); const query = exportContext.packageQuery();
if (query) { if (query) {
const isFragment = query.FRAGMENT().length > 0;
if (isFragment && (query.FRAGMENT().length !== 2 || query.typeParameters() || query.WORKSPACE()))
loweringIssue(
state,
query,
"invalid-query-fragment",
"Export a fragment on an exact object interface; generic fragments and workspace fragment roots are not supported",
);
const previousParameters = state.types.parameters; const previousParameters = state.types.parameters;
state.types.parameters = new Map(); state.types.parameters = new Map();
try { try {
@@ -1403,7 +1411,19 @@ const lowerPackage = (
displayName: identifier(query.identifier()), displayName: identifier(query.identifier()),
root: { kind: query.WORKSPACE() ? "workspace-interface" : "object", interface: close(query.interfaceType()) }, root: { kind: query.WORKSPACE() ? "workspace-interface" : "object", interface: close(query.interfaceType()) },
document: stringValue(query.stringLiteral(1)), document: stringValue(query.stringLiteral(1)),
operation: stringValue(query.stringLiteral(2)), operation: isFragment
? query.stringLiteral(3)
? stringValue(query.stringLiteral(3))
: "QuixosFragment"
: stringValue(query.stringLiteral(2)),
...(isFragment
? {
fragment: {
name: stringValue(query.stringLiteral(2)),
...(query.stringLiteral(3) ? { operation: stringValue(query.stringLiteral(3)) } : {}),
},
}
: {}),
fragments: [], fragments: [],
views: [], views: [],
allowances: [], allowances: [],
@@ -1436,8 +1456,32 @@ const lowerPackage = (
); );
if (sourceQueryId) declaration.placement = { kind: "local", sourceQueryId }; if (sourceQueryId) declaration.placement = { kind: "local", sourceQueryId };
} }
} else if (clause.FRAGMENTS()) declaration.fragments.push(stringValue(clause.stringLiteral()!)); } else if (clause.FRAGMENTS()) {
else if (clause.VIEW()) { if (clause.stringLiteral()) declaration.fragments.push(stringValue(clause.stringLiteral()!));
else {
const imported = lowerQueryReference(
state,
identifier(clause.identifier(0)),
identifier(clause.identifier(1)),
clause,
);
if (imported) {
const variables: Record<string, string> = {};
for (const mapping of clause.fragmentVariable()) {
const name = identifier(mapping.identifier(0));
if (Object.hasOwn(variables, name))
loweringIssue(state, mapping, "duplicate-fragment-variable", `Repeated fragment variable ${name}`);
variables[name] = identifier(mapping.identifier(1));
}
(declaration.importedFragments ??= []).push({
packageRevisionId: imported.packageRevisionId,
packageAlias: identifier(clause.identifier(0)),
fragmentId: imported.queryId,
variables,
});
}
}
} else if (clause.VIEW()) {
const name = identifier(clause.identifier(0)); const name = identifier(clause.identifier(0));
if (parameters.length) { if (parameters.length) {
const parameter = state.types.parameters.get(name); const parameter = state.types.parameters.get(name);
@@ -1548,6 +1592,23 @@ const lowerPackage = (
views: templateViews, views: templateViews,
allowances: templateAllowances, allowances: templateAllowances,
}); });
} else if (isFragment) {
if (declaration.placement || declaration.polling || declaration.allowances.length)
loweringIssue(
state,
query,
"invalid-query-fragment",
"Fragments are local projections with generated admission; remote allowances, polling and explicit placement are not supported",
);
const sourceQueryId = `${declaration.id}:fragment-inputs`;
queries.push({
...structuredClone(declaration),
id: sourceQueryId,
displayName: `${declaration.displayName}$inputs`,
placement: { kind: "replica" },
fragment: { ...declaration.fragment!, admission: true },
});
queries.push({ ...declaration, placement: { kind: "local", sourceQueryId } });
} else queries.push(declaration); } else queries.push(declaration);
return []; return [];
} finally { } finally {
+63 -7
View File
@@ -124,12 +124,64 @@ export function generateInterfaceClientBindings(
export function generatePackageQueryClientBindings( export function generatePackageQueryClientBindings(
pkg: PackageRevision, pkg: PackageRevision,
runtimeModule = "@quixos/camino-replica-core", runtimeModule = "@quixos/camino-replica-core",
packages: readonly PackageRevision[] = [pkg],
): string { ): string {
const queries = (pkg.checkedQueries ?? []).map( const queries = (pkg.checkedQueries ?? [])
(query) => .filter((query) => !query.declaration.fragment)
`${JSON.stringify(query.declaration.displayName)}: definePackageQuery<${tsType(query.variables)}, ${tsType(query.output)}, ${JSON.stringify(publicQueryContract(pkg, query.declaration.id).placement)}>(${JSON.stringify(publicQueryContract(pkg, query.declaration.id))})`, .map(
); (query) =>
return `// Generated from checked package query contracts. Regenerate; do not edit.\nimport { definePackageQuery, type ManagedReference } from ${JSON.stringify(runtimeModule)};\nexport const queries = {${queries.join(",\n")}} as const;\n`; `${JSON.stringify(query.declaration.displayName)}: definePackageQuery<${tsType(query.variables)}, ${tsType(query.output)}, ${JSON.stringify(publicQueryContract(pkg, query.declaration.id).placement)}>(${JSON.stringify(publicQueryContract(pkg, query.declaration.id))})`,
);
const emitFragment = (provider: PackageRevision, id: string) => {
const query = provider.checkedQueries?.find((query) => query.declaration.id === id);
if (
!query?.declaration.fragment ||
query.declaration.fragment.admission ||
query.declaration.placement?.kind !== "local" ||
query.output.kind !== "record" ||
!query.output.fields.root
)
throw new ExecutionPlanError("UNCHECKED_FRAGMENT", id, "Expected a checked local fragment export");
const output = query.output.fields.root;
const descriptor = {
package: provider.revisionId,
fragment: id,
digest: query.definitionDigest,
subject: query.declaration.root.interface,
input: portableValueSchema(query.variables),
output: portableValueSchema(output),
admission: publicQueryContract(provider, query.declaration.placement.sourceQueryId),
view: publicQueryContract(provider, id),
};
return `definePackageFragment<${tsType(query.variables)}, ${tsType(output)}, ${JSON.stringify(query.declaration.root.interface)}>(${JSON.stringify(descriptor)})`;
};
const fragments = (pkg.checkedQueries ?? [])
.filter((query) => query.declaration.fragment && !query.declaration.fragment.admission)
.map((query) => `${JSON.stringify(query.declaration.displayName)}: ${emitFragment(pkg, query.declaration.id)}`);
const imports = new Map<string, Map<string, string>>();
for (const query of pkg.queries ?? [])
for (const imported of query.importedFragments ?? []) {
if (imported.packageRevisionId === pkg.revisionId) continue;
const provider = packages.find((provider) => provider.revisionId === imported.packageRevisionId);
const name = provider?.queries?.find((query) => query.id === imported.fragmentId)?.displayName;
if (!provider || !name)
throw new ExecutionPlanError("UNCHECKED_FRAGMENT", imported.fragmentId, "Missing imported fragment provider");
const alias = imported.packageAlias ?? provider.displayName;
if (pkg.queries?.some((query) => query.fragment && query.displayName === alias))
throw new ExecutionPlanError(
"FRAGMENT_ALIAS_COLLISION",
alias,
"Imported fragment namespace conflicts with a local fragment export",
);
const entries = imports.get(alias) ?? new Map<string, string>();
entries.set(name, emitFragment(provider, imported.fragmentId));
imports.set(alias, entries);
}
for (const [alias, entries] of imports)
fragments.push(
`${JSON.stringify(alias)}: {${[...entries].map(([name, value]) => `${JSON.stringify(name)}: ${value}`).join(",")}}`,
);
return `// Generated from checked package query contracts. Regenerate; do not edit.\nimport { definePackageQuery, definePackageFragment, type ManagedReference } from ${JSON.stringify(runtimeModule)};\nexport const queries = {${queries.join(",\n")}} as const;\nexport const fragments = {${fragments.join(",\n")}} as const;\n`;
} }
/** Receipt-covered bindings expose the package's imports, component subjects and /** Receipt-covered bindings expose the package's imports, component subjects and
@@ -157,13 +209,17 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
.filter((line) => !line.startsWith("import ")) .filter((line) => !line.startsWith("import "))
.join("\n") .join("\n")
.replace("export const interfaces =", "export const subjects ="); .replace("export const interfaces =", "export const subjects =");
const queries = generatePackageQueryClientBindings(pkg, "@quixos/camino-react") const queries = generatePackageQueryClientBindings(
pkg,
"@quixos/camino-react",
plan.definition.workspace.packageImports,
)
.split("\n") .split("\n")
.filter((line) => !line.startsWith("import ")) .filter((line) => !line.startsWith("import "))
.join("\n"); .join("\n");
return ( return (
interfaces + interfaces +
'\nimport {definePackageQuery,type ComponentType,type ComponentProps} from "@quixos/camino-react";\n' + '\nimport {definePackageQuery,definePackageFragment,type ComponentType,type ComponentProps} from "@quixos/camino-react";\n' +
subjectBindings + subjectBindings +
"\n" + "\n" +
queries + queries +
+9
View File
@@ -30,6 +30,15 @@ export function compileQueryProgram(plan: ExecutionPlan): unknown {
const checked = query.checked; const checked = query.checked;
return { return {
id, id,
...(checked.declaration.fragment && !checked.declaration.fragment.admission
? {
fragment: {
package: query.packageRevisionId,
id: checked.declaration.id,
digest: checked.definitionDigest,
},
}
: {}),
placement: checked.declaration.placement?.kind ?? "remote", placement: checked.declaration.placement?.kind ?? "remote",
source: source:
checked.declaration.placement?.kind === "local" checked.declaration.placement?.kind === "local"
+30 -7
View File
@@ -37,6 +37,7 @@ import { querySchema } from "./schema.js";
import { objectRow } from "./relational-schema.js"; import { objectRow } from "./relational-schema.js";
import { workspaceQueryRootId } from "./root.js"; import { workspaceQueryRootId } from "./root.js";
import { relationalCompiler } from "./relational-compile.js"; import { relationalCompiler } from "./relational-compile.js";
import { fragmentSources, type FragmentResolver } from "./fragments.js";
import { import {
QueryCompileError, QueryCompileError,
type QueryDeclaration, type QueryDeclaration,
@@ -67,6 +68,7 @@ export async function compileQuery(
declaration: QueryDeclaration, declaration: QueryDeclaration,
interfaces: readonly InterfaceRevision[], interfaces: readonly InterfaceRevision[],
read: (name: string) => Promise<string>, read: (name: string) => Promise<string>,
resolveFragment?: FragmentResolver,
): Promise<CheckedQuery> { ): Promise<CheckedQuery> {
const paths = [declaration.document, ...declaration.fragments]; const paths = [declaration.document, ...declaration.fragments];
if (paths.length > 128 || new Set(paths).size !== paths.length) if (paths.length > 128 || new Set(paths).size !== paths.length)
@@ -86,7 +88,10 @@ export async function compileQuery(
const document = parse(new Source(text, path), { maxTokens: 10000 }); const document = parse(new Source(text, path), { maxTokens: 10000 });
definitions.push(...document.definitions); definitions.push(...document.definitions);
} }
const fragmentContracts = await fragmentSources(declaration, definitions, resolveFragment);
const document: DocumentNode = { kind: "Document" as DocumentNode["kind"], definitions }; const document: DocumentNode = { kind: "Document" as DocumentNode["kind"], definitions };
if (definitions.length > 256 || Buffer.byteLength(print(document)) > 262144)
fail("QUERY_SOURCE_LIMIT", "Expanded fragment sources exceed the query source budget");
visit(document, { visit(document, {
Directive(node) { Directive(node) {
if (node.name.value === "live") if (node.name.value === "live")
@@ -112,7 +117,7 @@ export async function compileQuery(
if (node.name.value.startsWith("__")) fail("QUERY_UNSUPPORTED_FEATURE", "Introspection is not supported", node); if (node.name.value.startsWith("__")) fail("QUERY_UNSUPPORTED_FEATURE", "Introspection is not supported", node);
}, },
Directive(node) { Directive(node) {
if (!["include", "skip", "live"].includes(node.name.value)) if (!["include", "skip"].includes(node.name.value))
fail("QUERY_UNSUPPORTED_FEATURE", `Unsupported directive ${node.name.value}`, node); fail("QUERY_UNSUPPORTED_FEATURE", `Unsupported directive ${node.name.value}`, node);
}, },
InlineFragment(node) { InlineFragment(node) {
@@ -400,11 +405,27 @@ export async function compileQuery(
inherited: QuerySelection["conditions"] = [], inherited: QuerySelection["conditions"] = [],
): QuerySelection[] => ): QuerySelection[] =>
set.selections.flatMap((node): QuerySelection[] => { set.selections.flatMap((node): QuerySelection[] => {
if (node.kind === "FragmentSpread") if (node.kind === "FragmentSpread") {
return selection(fragments.get(node.name.value)!.selectionSet, parent, [ const inheritedConditions = [...inherited, ...conditions(node.directives)];
...inherited, const contract = fragmentContracts.get(node.name.value);
...conditions(node.directives), if (contract && generated.byName.get(parent.name)?.revisionId !== contract.root)
]); fail("FRAGMENT_ROOT_MISMATCH", "Imported fragment requires its exact nominal root interface", node);
return [
...(contract
? [
{
name: "$fragment",
key: "",
selection: [],
arguments: {},
conditions: inheritedConditions,
fragmentSpread: contract,
},
]
: []),
...selection(fragments.get(node.name.value)!.selectionSet, parent, inheritedConditions),
];
}
if (node.kind !== "Field") return []; if (node.kind !== "Field") return [];
const contract = generated.byName.get(parent.name); const contract = generated.byName.get(parent.name);
const member = contract?.members.find((entry) => entry.displayName === node.name.value); const member = contract?.members.find((entry) => entry.displayName === node.name.value);
@@ -471,6 +492,7 @@ export async function compileQuery(
.map((entry) => [entry.variable.name.value, argument(entry.defaultValue!)]), .map((entry) => [entry.variable.name.value, argument(entry.defaultValue!)]),
), ),
sourceFiles: paths, sourceFiles: paths,
...(fragmentContracts.size ? { fragmentContracts: Object.fromEntries(fragmentContracts) } : {}),
}; };
} }
@@ -478,10 +500,11 @@ export const compileRepositoryQuery = async (
root: string, root: string,
declaration: QueryDeclaration, declaration: QueryDeclaration,
interfaces: readonly InterfaceRevision[], interfaces: readonly InterfaceRevision[],
resolveFragment?: FragmentResolver,
) => { ) => {
const started = performance.now(); const started = performance.now();
try { try {
return await compileQuery(declaration, interfaces, (name) => readRepositorySource(root, name)); return await compileQuery(declaration, interfaces, (name) => readRepositorySource(root, name), resolveFragment);
} finally { } finally {
// Timing belongs in check logs, never in immutable artifact identities. // Timing belongs in check logs, never in immutable artifact identities.
console.error( console.error(
+120
View File
@@ -0,0 +1,120 @@
import { parse, print, visit, type DocumentNode, type FragmentDefinitionNode } from "graphql";
import { QueryCompileError, type CheckedQuery, type QueryDeclaration, type QuerySelection } from "./types.js";
export type FragmentResolver = (packageRevisionId: string, fragmentId: string) => Promise<CheckedQuery>;
/** Expand only explicitly imported exports. Each variable mapping is checked by
* GraphQL against the consuming operation, rather than silently inheriting names. */
export async function fragmentSources(
declaration: QueryDeclaration,
definitions: DocumentNode["definitions"][number][],
resolve?: FragmentResolver,
) {
const markers = new Map<string, NonNullable<QuerySelection["fragmentSpread"]>>();
for (const imported of declaration.importedFragments ?? []) {
if (!resolve)
throw new QueryCompileError(
"FRAGMENT_RESOLVER_REQUIRED",
"Resolve imported fragments through package dependencies",
);
const checked = await resolve(imported.packageRevisionId, imported.fragmentId);
const exported = checked.declaration.fragment;
if (!exported || exported.admission) throw new QueryCompileError("NOT_EXPORTED_FRAGMENT", imported.fragmentId);
const variables = checked.variables.kind === "record" ? Object.keys(checked.variables.fields) : [];
if (JSON.stringify(Object.keys(imported.variables).sort()) !== JSON.stringify(variables.sort()))
throw new QueryCompileError(
"FRAGMENT_VARIABLE_MAPPING",
`Map every variable of ${checked.declaration.displayName} explicitly`,
);
const source = parse(checked.document);
const sourceOperation = source.definitions.find(
(node) => node.kind === "OperationDefinition" && node.name?.value === checked.declaration.operation,
);
const consumingOperation = definitions.find(
(node) => node.kind === "OperationDefinition" && node.name?.value === declaration.operation,
);
for (const variable of sourceOperation?.kind === "OperationDefinition"
? (sourceOperation.variableDefinitions ?? [])
: []) {
const name = imported.variables[variable.variable.name.value];
const target =
consumingOperation?.kind === "OperationDefinition"
? consumingOperation.variableDefinitions?.find((v) => v.variable.name.value === name)
: undefined;
if (!target || print(target.type) !== print(variable.type))
throw new QueryCompileError(
"FRAGMENT_VARIABLE_TYPE",
`Mapped variable ${name} must have type ${print(variable.type)}`,
);
}
const fragments = source.definitions.filter(
(node): node is FragmentDefinitionNode => node.kind === "FragmentDefinition",
);
if (definitions.some((node) => "name" in node && node.name?.value === exported.name))
throw new QueryCompileError("FRAGMENT_NAME_COLLISION", exported.name);
const privateNames = new Map(
fragments.map((node) => [
node.name.value,
node.name.value === exported.name ? exported.name : `${exported.name}__${node.name.value}`,
]),
);
for (const fragment of fragments) {
definitions.push(
visit(fragment, {
FragmentDefinition(node) {
return { ...node, name: { ...node.name, value: privateNames.get(node.name.value)! } };
},
FragmentSpread(node) {
return { ...node, name: { ...node.name, value: privateNames.get(node.name.value) ?? node.name.value } };
},
Variable(node) {
return { ...node, name: { ...node.name, value: imported.variables[node.name.value] ?? node.name.value } };
},
}),
);
}
markers.set(exported.name, {
package: imported.packageRevisionId,
fragment: imported.fragmentId,
digest: checked.definitionDigest,
root: checked.declaration.root.interface,
variables: imported.variables,
});
for (const [name, contract] of Object.entries(checked.fragmentContracts ?? {})) {
markers.set(privateNames.get(name) ?? name, {
...contract,
variables: Object.fromEntries(
Object.entries(contract.variables).map(([inner, outer]) => [inner, imported.variables[outer] ?? outer]),
),
});
}
}
if (declaration.fragment) {
const exported = declaration.fragment;
if (!definitions.some((node) => node.kind === "FragmentDefinition" && node.name.value === exported.name))
throw new QueryCompileError("FRAGMENT_MISSING", exported.name);
const operations = definitions.filter((node) => node.kind === "OperationDefinition");
if (!exported.operation) {
if (operations.length)
throw new QueryCompileError(
"FRAGMENT_OPERATION_REQUIRED",
"Name the standalone operation explicitly when a fragment document includes variables or an operation",
);
definitions.push(...parse(`query ${declaration.operation} { root { ...${exported.name} } }`).definitions);
} else {
const operation = operations.find(
(node) => node.kind === "OperationDefinition" && node.name?.value === exported.operation,
);
if (!operation || operation.kind !== "OperationDefinition")
throw new QueryCompileError("FRAGMENT_OPERATION_MISSING", exported.operation);
// The standalone operation specifies variables only. Its result must be the
// exact exported fragment, not additional fields or a different window.
const expected = parse(`query ${declaration.operation} { root { ...${exported.name} } }`).definitions[0]!;
if (expected.kind !== "OperationDefinition" || print(operation.selectionSet) !== print(expected.selectionSet))
throw new QueryCompileError(
"FRAGMENT_STANDALONE_SHAPE",
"Standalone operation must select root { ...ExportedFragment }",
);
}
}
return markers;
}
+17
View File
@@ -35,6 +35,14 @@ export type QueryRoot<Interface = InterfaceRevisionId> =
| { kind: "object"; interface: Interface } | { kind: "object"; interface: Interface }
| { kind: "workspace-interface"; interface: Interface }; | { kind: "workspace-interface"; interface: Interface };
export interface QueryDeclaration { export interface QueryDeclaration {
/** Exported local projection; compilation also creates its direct admission. */
fragment?: { name: string; operation?: string; admission?: boolean };
importedFragments?: {
packageRevisionId: string;
packageAlias?: string;
fragmentId: string;
variables: Record<string, string>;
}[];
placement?: { kind: "replica" } | { kind: "local"; sourceQueryId: string }; placement?: { kind: "replica" } | { kind: "local"; sourceQueryId: string };
id: string; id: string;
displayName: string; displayName: string;
@@ -83,6 +91,7 @@ export class QueryCompileError extends Error {
} }
} }
export interface CheckedQuery { export interface CheckedQuery {
fragmentContracts?: Record<string, NonNullable<QuerySelection["fragmentSpread"]>>;
declaration: QueryDeclaration; declaration: QueryDeclaration;
definitionDigest: string; definitionDigest: string;
/** Fixed, validated document: never supplied by runtime callers. */ /** Fixed, validated document: never supplied by runtime callers. */
@@ -102,6 +111,14 @@ 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 {
/** A checked occurrence marker, not an output field or storage capability. */
fragmentSpread?: {
package: string;
fragment: string;
digest: string;
root: InterfaceRevisionId;
variables: Record<string, string>;
};
name: string; name: string;
key: string; key: string;
interfaceRevisionId?: InterfaceRevisionId; interfaceRevisionId?: InterfaceRevisionId;
+47
View File
@@ -218,3 +218,50 @@ package Runtime id "package:runtime" revision "package:runtime@1" { }
/requires external interface Named \(interface:named@1\)/, /requires external interface Named \(interface:named@1\)/,
); );
}); });
test("resource-owned fragment imports compile through the locked package closure", async (context) => {
const directory = await mkdtemp(path.join(os.tmpdir(), "quixos-fragment-assembly-"));
context.after(() => rm(directory, { recursive: true, force: true }));
const dirs = Object.fromEntries(["item", "rows", "screen"].map((name) => [name, path.join(directory, name)]));
await Promise.all(Object.values(dirs).map((dir) => mkdir(dir)));
const dependency = (kind: string, name: string) =>
`${kind} ${name} source {repository "https://example.test/${name.toLowerCase()}.git"; commit "${namedCommit}";}`;
await writeFile(path.join(dirs.item!, "quixos.lock"), lock());
await writeFile(
path.join(dirs.item!, "interface.qx"),
`interface Item id "item" revision "item@1" {queryable value title id "title" : string {get id "title:get" execution snapshot targets [browser,server];}}`,
);
await writeFile(path.join(dirs.rows!, "quixos.lock"), lock(dependency("interface", "Item")));
await writeFile(
path.join(dirs.rows!, "package.qx"),
`import interface Item; package Rows id "rows" revision "rows@1" {query fragment Row id "row" root Item document "row.graphql" fragment "Row";}`,
);
await writeFile(path.join(dirs.rows!, "row.graphql"), "fragment Row on Item {_camino {ref} title}");
await writeFile(
path.join(dirs.screen!, "quixos.lock"),
lock(dependency("interface", "Item") + dependency("package", "Rows")),
);
await writeFile(
path.join(dirs.screen!, "package.qx"),
`import interface Item; import package Rows; package Screen id "screen" revision "screen@1" {
query Inputs id "inputs" root Item document "screen.graphql" operation "Screen" {replica; fragments Rows.Row;}
query View id "view" root Item document "screen.graphql" operation "Screen" {local from Inputs; fragments Rows.Row;}
}`,
);
await writeFile(path.join(dirs.screen!, "screen.graphql"), "query Screen {root {...Row}}");
const result = await compileCapabilityResourceRepository({
rootDirectory: dirs.screen!,
kind: "package",
source: { resolver: "git", repository: "https://example.test/screen.git", commit: packageCommit },
resolveResource: async (source) => {
const name = new URL(source.repository).pathname.slice(1, -4);
assert.ok(dirs[name], name);
return { directory: dirs[name]! };
},
});
assert.equal(result.resource.kind, "package");
if (result.resource.kind !== "package") throw Error("expected package");
const view = result.resource.revision.checkedQueries!.find((q) => q.declaration.id === "view")!;
assert.equal(view.selection[0]!.selection[0]!.fragmentSpread?.package, "rows@1");
assert.equal(result.resources.length, 3);
});
+184
View File
@@ -0,0 +1,184 @@
import assert from "node:assert/strict";
import test from "node:test";
import { compileCapabilityResourceSource } from "../src/capability-language/parser.js";
import { compileQuery } from "../src/query/compile.js";
import type { PackageRevision } from "../src/capability-model/types.js";
const source = { repository: "https://example.test/fragments", commit: "a".repeat(40) };
const compiled = compileCapabilityResourceSource(
`interface Item id "item" revision "item@1" {
queryable value title id "title" : string {get id "title:get" execution snapshot targets [browser,server];}
queryable rpc value remote id "remote" : string {get id "remote:get";}
}`,
{ source },
);
if (!compiled.ok || compiled.resource.kind !== "interface") throw Error(JSON.stringify(compiled.diagnostics));
const item = compiled.resource.revision;
function pkg(body: string, imports: PackageRevision[] = [], name = "Views") {
const result = compileCapabilityResourceSource(
`import interface Item; ${imports.map((p) => `import package ${p.displayName};`).join(" ")} package ${name} id "${name}" revision "${name}@1" {${body}}`,
{
source,
environment: {
interfaces: new Map([["Item", item]]),
interfaceClosure: [item],
packages: new Map(imports.map((p) => [p.displayName, p])),
},
},
);
assert.ok(result.ok, JSON.stringify(result.diagnostics));
if (result.resource.kind !== "package") throw Error("package required");
return result.resource.revision;
}
test("exported fragments generate local projection and direct admission", async () => {
const p = pkg('query fragment Row id "row" root Item document "row.graphql" fragment "Row";');
assert.equal(p.queries!.length, 2);
const local = p.queries!.find((q) => !q.fragment?.admission)!;
assert.equal(local.placement?.kind, "local");
const checked = await compileQuery(local, [item], async () => "fragment Row on Item {_camino {ref} title}");
assert.equal(checked.output.kind, "record");
assert.match(checked.document, /query QuixosFragment/);
await assert.rejects(
compileQuery(local, [item], async () => "fragment Row on Item {remote}"),
/QUERY_LOCAL_DEPENDENCY/,
);
});
test("imported fragments retain checked occurrences and require exact roots", async () => {
const provider = pkg('query fragment Row id "row" root Item document "row.graphql" fragment "Row";', [], "Provider");
const checked = await compileQuery(
provider.queries!.find((q) => !q.fragment?.admission)!,
[item],
async () => "fragment Row on Item {_camino {ref} title}",
);
const consumer = pkg(
'query Screen id "screen" root Item document "screen.graphql" operation "Screen" {replica; fragments Provider.Row;}',
[provider],
);
const result = await compileQuery(
consumer.queries![0]!,
[item],
async () => "query Screen {root {...Row}}",
async () => checked,
);
assert.equal(result.selection[0]!.selection[0]!.fragmentSpread?.digest, checked.definitionDigest);
assert.equal(result.selection[0]!.selection[0]!.fragmentSpread?.root, item.revisionId);
assert.ok(result.effects.some((e) => e.memberId === "title"));
const wrong = {
...checked,
declaration: {
...checked.declaration,
root: { ...checked.declaration.root, interface: "different@1" as typeof item.revisionId },
},
};
await assert.rejects(
compileQuery(
consumer.queries![0]!,
[item],
async () => "query Screen {root {...Row}}",
async () => wrong,
),
/FRAGMENT_ROOT_MISMATCH/,
);
});
test("fragment variable mappings are explicit and checked", async () => {
const provider = pkg(
'query fragment Row id "row" root Item document "row.graphql" fragment "Row" operation "Inputs";',
[],
"Provider",
);
const checked = await compileQuery(
provider.queries!.find((q) => !q.fragment?.admission)!,
[item],
async () =>
"query Inputs($show: Boolean!) {root {...Row}} fragment Row on Item {_camino {ref} title @include(if:$show)}",
);
const build = (mapping: string) =>
pkg(
`query Screen id "screen" root Item document "screen.graphql" operation "Screen" {replica; fragments Provider.Row ${mapping};}`,
[provider],
).queries![0]!;
const document = "query Screen($visible: Boolean!) {root {...Row}}";
await assert.rejects(
compileQuery(
build(""),
[item],
async () => document,
async () => checked,
),
/FRAGMENT_VARIABLE_MAPPING/,
);
const result = await compileQuery(
build("with {show to visible;}"),
[item],
async () => document,
async () => checked,
);
assert.deepEqual(result.selection[0]!.selection[0]!.fragmentSpread?.variables, { show: "visible" });
await assert.rejects(
compileQuery(
build("with {show to visible;}"),
[item],
async () => document.replace("Boolean!", "Boolean"),
async () => checked,
),
/FRAGMENT_VARIABLE_TYPE/,
);
});
test("independent fragment package survives execution lowering and typed client generation", async () => {
const { loadReaderWorkspace, readReaderFixture } = await import("./reader-fixture.js");
const { compileExecutionPlan } = await import("../src/execution-world/plan.js");
const { compileQueryProgram } = await import("../src/execution-world/queries.js");
const { generatePackageQueryClientBindings } = await import("../src/execution-world/client-contracts.js");
const workspace = await loadReaderWorkspace();
const comment = workspace.interfaceImports.find((i) => i.displayName === "CommentData")!;
const parsed = compileCapabilityResourceSource(
`import interface CommentData; package CommentRows id "comment-rows" revision "comment-rows@1" {
query fragment Row id "row" root CommentData document "row.graphql" fragment "CommentRow";
}`,
{ source, environment: { interfaces: new Map([["CommentData", comment]]), interfaceClosure: [comment] } },
);
assert.ok(parsed.ok, JSON.stringify(parsed.diagnostics));
if (parsed.resource.kind !== "package") throw Error("expected package");
const provider = parsed.resource.revision;
provider.checkedQueries = await Promise.all(
provider.queries!.map((q) =>
compileQuery(
q,
workspace.interfaceImports,
async () => "fragment CommentRow on CommentData {_camino {ref} content {text tags}}",
),
),
);
workspace.packageImports.push(provider);
const consumer = workspace.packageImports.find((p) => p.displayName === "ReaderRuntime")!;
const declaration = consumer.queries!.find((q) => q.displayName === "ReaderScreen")!;
declaration.importedFragments = [
{ packageRevisionId: provider.revisionId, packageAlias: "CommentRows", fragmentId: "row", variables: {} },
];
const document = (await readReaderFixture("queries/reader-screen.graphql")).replace(
"_camino { ref } content { text tags }",
"...CommentRow",
);
const checked = await compileQuery(
declaration,
workspace.interfaceImports,
async () => document,
async () => provider.checkedQueries!.find((q) => q.declaration.id === "row")!,
);
consumer.checkedQueries = consumer.checkedQueries!.map((q) => (q.declaration.id === declaration.id ? checked : q));
const plan = compileExecutionPlan(workspace);
const program = compileQueryProgram(plan) as {
queries: { id: string; fragment?: { package: string; id: string }; selection: unknown }[];
};
assert.deepEqual(program.queries.find((q) => q.fragment)?.fragment, {
package: provider.revisionId,
id: "row",
digest: provider.checkedQueries!.find((q) => q.declaration.id === "row")!.definitionDigest,
});
const screen = program.queries.find((q) => q.id === JSON.stringify([consumer.revisionId, declaration.id]))!;
assert.match(JSON.stringify(screen.selection), /fragmentSpread/);
const generated = generatePackageQueryClientBindings(consumer, undefined, workspace.packageImports);
assert.match(generated, /"CommentRows": \{"Row": definePackageFragment/);
assert.ok(generated.includes(JSON.stringify(comment.revisionId)));
assert.ok(!generated.includes('"Row$inputs": definePackageQuery'));
});