Add exported query fragments and scoped coverage observations
This commit is contained in:
@@ -276,12 +276,37 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
|
||||
await checkQueryTemplate(template, queryInterfaces, (name) =>
|
||||
readRepositorySource(snapshot.directory, name),
|
||||
);
|
||||
if (compiled.resource.revision.queries?.length)
|
||||
compiled.resource.revision.checkedQueries = await Promise.all(
|
||||
compiled.resource.revision.queries.map((query) =>
|
||||
compileRepositoryQuery(snapshot.directory, query, queryInterfaces),
|
||||
),
|
||||
);
|
||||
if (compiled.resource.revision.queries?.length) {
|
||||
const pkg = compiled.resource.revision;
|
||||
const checked = new Map<string, import("../query/types.js").CheckedQuery>();
|
||||
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;
|
||||
try {
|
||||
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 { PackageQuerySpecializationContext } from "./QuixosCapabilityParser.js";
|
||||
import { QueryClauseContext } from "./QuixosCapabilityParser.js";
|
||||
import { FragmentVariableContext } from "./QuixosCapabilityParser.js";
|
||||
import { PackageOperationExportContext } from "./QuixosCapabilityParser.js";
|
||||
import { PackageFunctionExportContext } from "./QuixosCapabilityParser.js";
|
||||
import { AcceptanceClauseContext } from "./QuixosCapabilityParser.js";
|
||||
@@ -291,6 +292,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
|
||||
* @return the visitor 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`.
|
||||
* @param ctx the parse tree
|
||||
|
||||
@@ -1380,6 +1380,14 @@ const lowerPackage = (
|
||||
if (exportContext.packageQuerySpecialization()) return [];
|
||||
const query = exportContext.packageQuery();
|
||||
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;
|
||||
state.types.parameters = new Map();
|
||||
try {
|
||||
@@ -1403,7 +1411,19 @@ const lowerPackage = (
|
||||
displayName: identifier(query.identifier()),
|
||||
root: { kind: query.WORKSPACE() ? "workspace-interface" : "object", interface: close(query.interfaceType()) },
|
||||
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: [],
|
||||
views: [],
|
||||
allowances: [],
|
||||
@@ -1436,8 +1456,32 @@ const lowerPackage = (
|
||||
);
|
||||
if (sourceQueryId) declaration.placement = { kind: "local", sourceQueryId };
|
||||
}
|
||||
} else if (clause.FRAGMENTS()) declaration.fragments.push(stringValue(clause.stringLiteral()!));
|
||||
else if (clause.VIEW()) {
|
||||
} else if (clause.FRAGMENTS()) {
|
||||
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));
|
||||
if (parameters.length) {
|
||||
const parameter = state.types.parameters.get(name);
|
||||
@@ -1548,6 +1592,23 @@ const lowerPackage = (
|
||||
views: templateViews,
|
||||
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);
|
||||
return [];
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user