Implement query execution, scoped RPC enrichment, live collections, and scaffold integration
This commit is contained in:
@@ -111,7 +111,7 @@ valueMemberOperation
|
|||||||
;
|
;
|
||||||
|
|
||||||
relationshipMember
|
relationshipMember
|
||||||
: queryReadContract? RELATION identifier ID stringLiteral COLON cardinality targetConstraint ORDERED?
|
: queryReadContract? RELATION identifier ID stringLiteral COLON cardinality targetConstraint ORDERED? (KEYED stringLiteral)?
|
||||||
LBRACE relationshipOperation* RBRACE
|
LBRACE relationshipOperation* RBRACE
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -143,16 +143,21 @@ packageExport
|
|||||||
| packageFunctionExport
|
| packageFunctionExport
|
||||||
| packageConstructorExport
|
| packageConstructorExport
|
||||||
| packageQuery
|
| packageQuery
|
||||||
|
| packageQuerySpecialization
|
||||||
;
|
;
|
||||||
|
|
||||||
packageQuery
|
packageQuery
|
||||||
: QUERY identifier ID stringLiteral ROOT interfaceType
|
: QUERY identifier typeParameters? ID stringLiteral ROOT interfaceType
|
||||||
DOCUMENT stringLiteral OPERATION stringLiteral LBRACE queryClause* RBRACE
|
DOCUMENT stringLiteral OPERATION stringLiteral LBRACE queryClause* RBRACE
|
||||||
;
|
;
|
||||||
|
|
||||||
|
packageQuerySpecialization
|
||||||
|
: QUERY identifier ID stringLiteral SPECIALIZE identifier typeArguments SEMI
|
||||||
|
;
|
||||||
|
|
||||||
queryClause
|
queryClause
|
||||||
: FRAGMENTS stringLiteral SEMI
|
: FRAGMENTS stringLiteral SEMI
|
||||||
| VIEW 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
|
||||||
| WATCH SEMI
|
| WATCH SEMI
|
||||||
@@ -206,6 +211,7 @@ dependencyPort
|
|||||||
| EDGE identifier ID stringLiteral COLON cardinality targetConstraint primitiveList SEMI
|
| EDGE identifier ID stringLiteral COLON cardinality targetConstraint primitiveList SEMI
|
||||||
| INTERFACE identifier ID stringLiteral COLON identifier typeArguments? SEMI
|
| INTERFACE identifier ID stringLiteral COLON identifier typeArguments? SEMI
|
||||||
| CONSTRUCTOR identifier ID stringLiteral COLON identifier (INPUT valueType)? SEMI
|
| CONSTRUCTOR identifier ID stringLiteral COLON identifier (INPUT valueType)? SEMI
|
||||||
|
| QUERY identifier ID stringLiteral COLON identifier DOT identifier SEMI
|
||||||
;
|
;
|
||||||
|
|
||||||
primitiveList
|
primitiveList
|
||||||
@@ -322,6 +328,7 @@ dependencyBinding
|
|||||||
| identifier TO EDGE identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI
|
| identifier TO EDGE identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI
|
||||||
| identifier TO INTERFACE identifier typeArguments? (VIA EDGE identifier DOT identifier)? SEMI
|
| identifier TO INTERFACE identifier typeArguments? (VIA EDGE identifier DOT identifier)? SEMI
|
||||||
| identifier TO CONSTRUCTOR identifier SEMI
|
| identifier TO CONSTRUCTOR identifier SEMI
|
||||||
|
| identifier TO QUERY identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI
|
||||||
;
|
;
|
||||||
|
|
||||||
constructorBindingDecl
|
constructorBindingDecl
|
||||||
@@ -391,6 +398,7 @@ identifier
|
|||||||
: IDENTIFIER
|
: IDENTIFIER
|
||||||
| SOURCE
|
| SOURCE
|
||||||
| QUERY
|
| QUERY
|
||||||
|
| SPECIALIZE
|
||||||
| ROOT
|
| ROOT
|
||||||
| DOCUMENT
|
| DOCUMENT
|
||||||
| FRAGMENTS
|
| FRAGMENTS
|
||||||
@@ -408,6 +416,7 @@ stringLiteral
|
|||||||
|
|
||||||
WORKSPACE: 'workspace';
|
WORKSPACE: 'workspace';
|
||||||
QUERY: 'query';
|
QUERY: 'query';
|
||||||
|
SPECIALIZE: 'specialize';
|
||||||
ROOT: 'root';
|
ROOT: 'root';
|
||||||
DOCUMENT: 'document';
|
DOCUMENT: 'document';
|
||||||
FRAGMENTS: 'fragments';
|
FRAGMENTS: 'fragments';
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ package camino;
|
|||||||
import "camino/schema.proto";
|
import "camino/schema.proto";
|
||||||
|
|
||||||
service CaminoService {
|
service CaminoService {
|
||||||
|
rpc ExecuteQuery(QueryRequest) returns (QueryResponse);
|
||||||
|
rpc QueryChanges(QueryChangesRequest) returns (QueryChangesResponse);
|
||||||
rpc InstallPersistencePlan(InstallPersistencePlanRequest) returns (InstallPersistencePlanResponse);
|
rpc InstallPersistencePlan(InstallPersistencePlanRequest) returns (InstallPersistencePlanResponse);
|
||||||
rpc GetPersistencePlan(GetPersistencePlanRequest) returns (GetPersistencePlanResponse);
|
rpc GetPersistencePlan(GetPersistencePlanRequest) returns (GetPersistencePlanResponse);
|
||||||
rpc CreateObject(CreateObjectRequest) returns (CreateObjectResponse);
|
rpc CreateObject(CreateObjectRequest) returns (CreateObjectResponse);
|
||||||
@@ -214,3 +216,84 @@ message CaminoOp {
|
|||||||
string created_at = 6;
|
string created_at = 6;
|
||||||
string client_mutation_id = 7;
|
string client_mutation_id = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message QueryRequest {
|
||||||
|
string query_id = 1;
|
||||||
|
string object_id = 2;
|
||||||
|
map<string, Value> variables = 3;
|
||||||
|
// Typed callers require this exact checked definition. Administrative/raw
|
||||||
|
// callers may omit it to explicitly select the currently installed definition.
|
||||||
|
string expected_definition_digest = 4;
|
||||||
|
}
|
||||||
|
message QueryPathPart {
|
||||||
|
oneof part {
|
||||||
|
string field = 1;
|
||||||
|
uint32 index = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message QueryPendingField {
|
||||||
|
repeated QueryPathPart path = 1;
|
||||||
|
string object_id = 2;
|
||||||
|
string interface_revision_id = 3;
|
||||||
|
string member_id = 4;
|
||||||
|
string operation_id = 5;
|
||||||
|
string value_type_json = 6;
|
||||||
|
// Internal residual facts are separate from the authored result projection.
|
||||||
|
optional uint32 residual_window = 7;
|
||||||
|
uint32 residual_row = 8;
|
||||||
|
string residual_field = 9;
|
||||||
|
}
|
||||||
|
message QueryStats {
|
||||||
|
uint32 sql_count = 1;
|
||||||
|
double sql_ms = 2;
|
||||||
|
uint32 rpc_count = 3;
|
||||||
|
double rpc_ms = 4;
|
||||||
|
uint32 result_bytes = 5;
|
||||||
|
double preparation_ms = 6;
|
||||||
|
double total_ms = 7;
|
||||||
|
}
|
||||||
|
message QueryResponse {
|
||||||
|
Value value = 1;
|
||||||
|
string data_version = 2;
|
||||||
|
string binding_digest = 3;
|
||||||
|
repeated QueryPendingField pending = 4;
|
||||||
|
QueryStats stats = 5;
|
||||||
|
// Redeemable only through the host's private control socket, not an RPC grant.
|
||||||
|
string preparation_token = 6;
|
||||||
|
repeated QueryFieldFailure errors = 7;
|
||||||
|
repeated QueryResidualWindow residual_windows = 8;
|
||||||
|
// Native reads share one database snapshot; package enrichment does not.
|
||||||
|
string consistency = 9; // native-snapshot | mixed
|
||||||
|
}
|
||||||
|
|
||||||
|
message QueryResidualRow {
|
||||||
|
string entry_id = 1;
|
||||||
|
map<string, Value> fields = 2;
|
||||||
|
}
|
||||||
|
message QueryResidualOrder {
|
||||||
|
string field = 1;
|
||||||
|
bool descending = 2;
|
||||||
|
}
|
||||||
|
message QueryResidualWindow {
|
||||||
|
repeated QueryPathPart path = 1;
|
||||||
|
repeated QueryResidualRow rows = 2;
|
||||||
|
string predicate_json = 3;
|
||||||
|
repeated QueryResidualOrder order = 4;
|
||||||
|
uint32 limit = 5;
|
||||||
|
bool bounded_all = 6;
|
||||||
|
repeated QuerySelection selection = 7;
|
||||||
|
map<string, string> field_types = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message QueryFieldFailure {
|
||||||
|
repeated QueryPathPart path = 1;
|
||||||
|
string error = 2;
|
||||||
|
}
|
||||||
|
message QueryChangesRequest {
|
||||||
|
string query_id = 1;
|
||||||
|
string object_id = 2;
|
||||||
|
string data_version = 3;
|
||||||
|
}
|
||||||
|
message QueryChangesResponse {
|
||||||
|
bool changed = 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ message QueryReadBinding {
|
|||||||
string field_name = 11;
|
string field_name = 11;
|
||||||
string value_type_json = 12;
|
string value_type_json = 12;
|
||||||
Cardinality cardinality = 13;
|
Cardinality cardinality = 13;
|
||||||
|
string key_type = 14;
|
||||||
}
|
}
|
||||||
message QueryBudgets {
|
message QueryBudgets {
|
||||||
uint32 rows = 1;
|
uint32 rows = 1;
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ syntax = "proto3";
|
|||||||
package quixos.orch;
|
package quixos.orch;
|
||||||
|
|
||||||
import "camino/api.proto";
|
import "camino/api.proto";
|
||||||
|
import "camino/schema.proto";
|
||||||
import "quixos/package.proto";
|
import "quixos/package.proto";
|
||||||
import "quixos/refs.proto";
|
import "quixos/refs.proto";
|
||||||
import "quixos/runtime.proto";
|
import "quixos/runtime.proto";
|
||||||
|
|
||||||
service OrchestratorRuntime {
|
service OrchestratorRuntime {
|
||||||
|
rpc ExecuteQuery(camino.QueryRequest) returns (camino.QueryResponse);
|
||||||
|
rpc WatchQuery(camino.QueryRequest) returns (stream QueryEvent);
|
||||||
rpc TryConform(TryConformRequest) returns (TryConformResponse);
|
rpc TryConform(TryConformRequest) returns (TryConformResponse);
|
||||||
rpc InvokeCapability(InvokeCapabilityRequest) returns (InvokeCapabilityResponse);
|
rpc InvokeCapability(InvokeCapabilityRequest) returns (InvokeCapabilityResponse);
|
||||||
rpc EditCapabilityField(EditCapabilityFieldRequest) returns (InvokeCapabilityResponse);
|
rpc EditCapabilityField(EditCapabilityFieldRequest) returns (InvokeCapabilityResponse);
|
||||||
@@ -23,6 +26,16 @@ service OrchestratorRuntime {
|
|||||||
rpc CloseActivation(CloseActivationRequest) returns (CloseActivationResponse);
|
rpc CloseActivation(CloseActivationRequest) returns (CloseActivationResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message QueryEvent {
|
||||||
|
string run_id = 1;
|
||||||
|
uint64 sequence = 2;
|
||||||
|
string kind = 3;
|
||||||
|
camino.QueryResponse snapshot = 4;
|
||||||
|
repeated camino.QueryPathPart path = 5;
|
||||||
|
camino.Value value = 6;
|
||||||
|
string error = 7;
|
||||||
|
}
|
||||||
|
|
||||||
// Exact closed interface lookup; no policy selection or competing conformances.
|
// Exact closed interface lookup; no policy selection or competing conformances.
|
||||||
message TryConformRequest {
|
message TryConformRequest {
|
||||||
string object_id = 1;
|
string object_id = 1;
|
||||||
@@ -110,6 +123,7 @@ message WatchCapabilityEvent {
|
|||||||
message GetWorkspaceRequest {
|
message GetWorkspaceRequest {
|
||||||
// Revision polling must not download the entire interface graph.
|
// Revision polling must not download the entire interface graph.
|
||||||
bool include_interface_contracts = 1;
|
bool include_interface_contracts = 1;
|
||||||
|
bool include_query_contracts = 2;
|
||||||
}
|
}
|
||||||
message GetWorkspaceResponse {
|
message GetWorkspaceResponse {
|
||||||
string workspace_id = 1;
|
string workspace_id = 1;
|
||||||
@@ -123,6 +137,18 @@ message GetWorkspaceResponse {
|
|||||||
// Exact closed interface contracts used by checked presentation consumers.
|
// Exact closed interface contracts used by checked presentation consumers.
|
||||||
string interfaces_json = 7;
|
string interfaces_json = 7;
|
||||||
repeated ClassCapability class_capabilities = 8;
|
repeated ClassCapability class_capabilities = 8;
|
||||||
|
repeated QueryDescription queries = 9;
|
||||||
|
uint32 active_query_executions = 10;
|
||||||
|
}
|
||||||
|
message QueryDescription {
|
||||||
|
string id = 1;
|
||||||
|
string name = 2;
|
||||||
|
string package_revision_id = 3;
|
||||||
|
string document = 4;
|
||||||
|
string schema = 5;
|
||||||
|
repeated string source_files = 6;
|
||||||
|
string effects_json = 7;
|
||||||
|
camino.InstalledQuery plan = 8;
|
||||||
}
|
}
|
||||||
message ClassCapability {
|
message ClassCapability {
|
||||||
string conformance_id = 1;
|
string conformance_id = 1;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ message InjectedDependency {
|
|||||||
EdgeDependency edge = 3;
|
EdgeDependency edge = 3;
|
||||||
string interface_revision_id = 4;
|
string interface_revision_id = 4;
|
||||||
string constructor_atom_id = 5;
|
string constructor_atom_id = 5;
|
||||||
|
string query_id = 7;
|
||||||
}
|
}
|
||||||
// Defaults to the invocation receiver. A checked dependency traversal can
|
// Defaults to the invocation receiver. A checked dependency traversal can
|
||||||
// select a related object explicitly before the package runtime starts.
|
// select a related object explicitly before the package runtime starts.
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ export const genericImplementationType = (
|
|||||||
definition: GenericPackageExport,
|
definition: GenericPackageExport,
|
||||||
interfaces: InterfaceRevision[],
|
interfaces: InterfaceRevision[],
|
||||||
concrete: (type: ValueType) => string,
|
concrete: (type: ValueType) => string,
|
||||||
|
queryPort: (requirement: Extract<GenericDependencyPort["requirement"], { kind: "query" }>) => string = () => {
|
||||||
|
throw new Error("Query binding schema required");
|
||||||
|
},
|
||||||
): string => {
|
): string => {
|
||||||
const names = new Map(definition.parameters.map((parameter, index) => [parameter.id, `T${index}`]));
|
const names = new Map(definition.parameters.map((parameter, index) => [parameter.id, `T${index}`]));
|
||||||
type Scope = { arguments: Map<string, string>; aliases: ValueAliasDefinition[] };
|
type Scope = { arguments: Map<string, string>; aliases: ValueAliasDefinition[] };
|
||||||
@@ -108,6 +111,8 @@ export const genericImplementationType = (
|
|||||||
]);
|
]);
|
||||||
return object(methods);
|
return object(methods);
|
||||||
}
|
}
|
||||||
|
case "query":
|
||||||
|
return queryPort(requirement);
|
||||||
case "constructor":
|
case "constructor":
|
||||||
return object([
|
return object([
|
||||||
[
|
[
|
||||||
|
|||||||
+43
-3
@@ -144,6 +144,33 @@ export const generateTypeScriptBindings = (
|
|||||||
const port = (entry: DependencyPort): { type: string; spec: unknown } => {
|
const port = (entry: DependencyPort): { type: string; spec: unknown } => {
|
||||||
const requirement = entry.requirement;
|
const requirement = entry.requirement;
|
||||||
switch (requirement.kind) {
|
switch (requirement.kind) {
|
||||||
|
case "query": {
|
||||||
|
const query = schema.packages
|
||||||
|
.find((candidate) => candidate.revisionId === requirement.packageRevisionId)
|
||||||
|
?.checkedQueries?.find((query) => query.declaration.id === requirement.queryId);
|
||||||
|
if (!query) throw new Error(`Missing checked query ${requirement.packageRevisionId}:${requirement.queryId}`);
|
||||||
|
return {
|
||||||
|
type: object([
|
||||||
|
["execute", `(variables: ${type(query.variables)}) => Promise<${type(query.output)}>`],
|
||||||
|
...(query.declaration.watch
|
||||||
|
? ([
|
||||||
|
[
|
||||||
|
"watch",
|
||||||
|
`(variables: ${type(query.variables)}, signal: AbortSignal) => AsyncIterable<QxQuerySnapshot<${type(query.output)}>>`,
|
||||||
|
],
|
||||||
|
] as [string, string][])
|
||||||
|
: []),
|
||||||
|
]),
|
||||||
|
spec: {
|
||||||
|
kind: "query",
|
||||||
|
id: entry.id,
|
||||||
|
definitionDigest: query.definitionDigest,
|
||||||
|
variables: query.variables,
|
||||||
|
output: query.output,
|
||||||
|
watch: query.declaration.watch,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
case "state": {
|
case "state": {
|
||||||
const methods = requirement.primitives.map((primitive): [string, string] => {
|
const methods = requirement.primitives.map((primitive): [string, string] => {
|
||||||
if (primitive === "read") return ["get", `() => Promise<${type(requirement.valueType)}>`];
|
if (primitive === "read") return ["get", `() => Promise<${type(requirement.valueType)}>`];
|
||||||
@@ -282,7 +309,12 @@ export const generateTypeScriptBindings = (
|
|||||||
for (const definition of pkg.genericExports ?? []) {
|
for (const definition of pkg.genericExports ?? []) {
|
||||||
handlers.push([
|
handlers.push([
|
||||||
definition.displayName,
|
definition.displayName,
|
||||||
genericImplementationType(definition, [...schema.interfaces, ...(schema.interfaceTemplates ?? [])], type),
|
genericImplementationType(
|
||||||
|
definition,
|
||||||
|
[...schema.interfaces, ...(schema.interfaceTemplates ?? [])],
|
||||||
|
type,
|
||||||
|
(requirement) => port({ id: capabilityId.dependencyPort("query"), displayName: "query", requirement }).type,
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
// Unused imported interfaces must not introduce new message-codec obligations.
|
// Unused imported interfaces must not introduce new message-codec obligations.
|
||||||
@@ -326,7 +358,13 @@ export const generateTypeScriptBindings = (
|
|||||||
const queryResults = object(
|
const queryResults = object(
|
||||||
(pkg.checkedQueries ?? []).map((query) => [query.declaration.displayName, type(query.output)]),
|
(pkg.checkedQueries ?? []).map((query) => [query.declaration.displayName, type(query.output)]),
|
||||||
);
|
);
|
||||||
const signatures = `${object(contexts)} ${object(handlers)} ${object(results)} ${queryVariables} ${queryResults} ${contracts.map((entry) => entry.type).join(" ")}`;
|
const queryDescriptors = object(
|
||||||
|
(pkg.checkedQueries ?? []).map((query) => [
|
||||||
|
query.declaration.displayName,
|
||||||
|
`QxQueryDescriptor<QueryVariables[${q(query.declaration.displayName)}], QueryResults[${q(query.declaration.displayName)}], ${q(query.declaration.root)}>`,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const signatures = `${object(contexts)} ${object(handlers)} ${object(results)} ${queryVariables} ${queryResults} ${queryDescriptors} ${contracts.map((entry) => entry.type).join(" ")}`;
|
||||||
const typeImports = [
|
const typeImports = [
|
||||||
"BindingValue",
|
"BindingValue",
|
||||||
"QxObjectRef",
|
"QxObjectRef",
|
||||||
@@ -339,6 +377,8 @@ export const generateTypeScriptBindings = (
|
|||||||
"QxLiveValue",
|
"QxLiveValue",
|
||||||
"RelationshipCollection",
|
"RelationshipCollection",
|
||||||
"RelationshipEntry",
|
"RelationshipEntry",
|
||||||
|
"QxQuerySnapshot",
|
||||||
|
"QxQueryDescriptor",
|
||||||
].filter((name) => new RegExp(`\\b${name}\\b`).test(signatures));
|
].filter((name) => new RegExp(`\\b${name}\\b`).test(signatures));
|
||||||
return (
|
return (
|
||||||
`// Generated by quixos-codegen-ts. Do not edit. Binding ABI version 1.\n` +
|
`// Generated by quixos-codegen-ts. Do not edit. Binding ABI version 1.\n` +
|
||||||
@@ -348,7 +388,7 @@ export const generateTypeScriptBindings = (
|
|||||||
`declare const appliedType: unique symbol;\ntype QxApplied<Definition extends string, Arguments extends readonly unknown[]> = string & {readonly [appliedType]: (value: [Definition, Arguments]) => [Definition, Arguments]};\n` +
|
`declare const appliedType: unique symbol;\ntype QxApplied<Definition extends string, Arguments extends readonly unknown[]> = string & {readonly [appliedType]: (value: [Definition, Arguments]) => [Definition, Arguments]};\n` +
|
||||||
`export type Contexts = ${object(contexts)};\nexport type Results = ${object(results)};\nexport type Implementation = ${object(handlers)};\n` +
|
`export type Contexts = ${object(contexts)};\nexport type Results = ${object(results)};\nexport type Implementation = ${object(handlers)};\n` +
|
||||||
`export type QueryVariables = ${queryVariables};\nexport type QueryResults = ${queryResults};\n` +
|
`export type QueryVariables = ${queryVariables};\nexport type QueryResults = ${queryResults};\n` +
|
||||||
`export const queries = ${JSON.stringify(Object.fromEntries((pkg.checkedQueries ?? []).map((query) => [query.declaration.displayName, { id: `${pkg.revisionId}:${query.declaration.id}`, definitionDigest: query.definitionDigest, rootInterfaceRevisionId: query.declaration.root, variables: query.variables, output: query.output, watch: query.declaration.watch }])), null, 2)} as const;\n` +
|
`export const queries: ${queryDescriptors} = ${JSON.stringify(Object.fromEntries((pkg.checkedQueries ?? []).map((query) => [query.declaration.displayName, { id: `${pkg.revisionId}:${query.declaration.id}`, definitionDigest: query.definitionDigest, rootInterfaceRevisionId: query.declaration.root, variables: query.variables, output: query.output, watch: query.declaration.watch }])), null, 2)} as const;\n` +
|
||||||
`export const contracts = {${contracts.map((entry) => `${q(entry.iface.revisionId)}: ${entry.code}`).join(",\n")}};\n` +
|
`export const contracts = {${contracts.map((entry) => `${q(entry.iface.revisionId)}: ${entry.code}`).join(",\n")}};\n` +
|
||||||
`export const interfaces = {${contracts
|
`export const interfaces = {${contracts
|
||||||
.filter((entry) => contracts.filter((other) => other.iface.displayName === entry.iface.displayName).length === 1)
|
.filter((entry) => contracts.filter((other) => other.iface.displayName === entry.iface.displayName).length === 1)
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ declare module "@quixos/web-studio-react-runtime" {
|
|||||||
export function tryConform<View>(object: string | {readonly $quixosRef: string}, contract: ReactInterfaceContract<View>, options?: {signal?: AbortSignal}): Promise<View | undefined>;
|
export function tryConform<View>(object: string | {readonly $quixosRef: string}, contract: ReactInterfaceContract<View>, options?: {signal?: AbortSignal}): Promise<View | undefined>;
|
||||||
export type ConformanceResult<View> = {status: "loading"} | {status: "absent"} | {status: "available"; view: View} | {status: "error"; error: Error};
|
export type ConformanceResult<View> = {status: "loading"} | {status: "absent"} | {status: "available"; view: View} | {status: "error"; error: Error};
|
||||||
export function useTryConform<View>(object: string | {readonly $quixosRef: string}, contract: ReactInterfaceContract<View>): ConformanceResult<View>;
|
export function useTryConform<View>(object: string | {readonly $quixosRef: string}, contract: ReactInterfaceContract<View>): ConformanceResult<View>;
|
||||||
|
export type QueryValueType = {kind: "builtin" | "scalar"; name: string} | {kind: "record"; fields: Record<string, QueryValueType>} | {kind: "optional" | "list"; value: QueryValueType} | {kind: "object-ref"; expectation: {kind: "atom"; atomId: string} | {kind: "interface"; interfaceRevisionId: string}};
|
||||||
|
export type QueryReference<Contract extends string = string> = {readonly $quixosRef: string; readonly queryContract: Contract};
|
||||||
|
export interface QueryDescriptor<Variables, Result> {readonly id: string; readonly definitionDigest: string; readonly rootInterfaceRevisionId: string; readonly variables: QueryValueType; readonly output: QueryValueType; readonly watch: boolean; readonly $types?: (variables: Variables, result: Result) => [Variables, Result]}
|
||||||
|
export type QueryPartial<T> = T extends QueryReference ? T : T extends readonly (infer Item)[] ? QueryPartial<Item>[] : T extends object ? {[Key in keyof T]?: QueryPartial<T[Key]>} : T;
|
||||||
|
export type QueryFieldState = {path: readonly (string | number)[]; status: "pending" | "error"; error?: string};
|
||||||
|
export type QueryState<T> = {refreshing: boolean; fields: QueryFieldState[]; runId?: string; sequence?: bigint; consistency?: string} & ({status: "loading"; data?: undefined; error?: undefined} | {status: "ready"; data: T; error?: undefined} | {status: "partial"; data: QueryPartial<T>; error?: undefined} | {status: "error"; data?: T | QueryPartial<T>; error: Error});
|
||||||
|
export function useQuery<Variables, Result>(descriptor: QueryDescriptor<Variables, Result>, options: {root: string | {readonly $quixosRef: string}; variables: Variables}): QueryState<Result> & {refresh(): void};
|
||||||
|
export interface QueryConnection<Row> {entries: {key: string; node: Row; cursor?: string | null}[]; pageInfo: {hasNextPage: boolean; endCursor?: string | null}}
|
||||||
|
export function CollectionView<Result, Row>(props: {result: QueryState<Result> & {refresh(): void}; connection(data: QueryPartial<Result>): QueryPartial<QueryConnection<Row>> | undefined; renderItem(row: QueryPartial<Row>, entryKey: string): React.ReactNode; onCreate?: () => void; onLoadMore?: (cursor: string | null) => void; onReset?: () => void; loadingMore?: boolean; empty?: React.ReactNode; prefetch?: boolean}): React.ReactElement;
|
||||||
export type ReactComponentHostProps<Action> = {
|
export type ReactComponentHostProps<Action> = {
|
||||||
onAction?: (action: Action) => void;
|
onAction?: (action: Action) => void;
|
||||||
fallback?: React.ReactNode;
|
fallback?: React.ReactNode;
|
||||||
|
|||||||
+32
-1
@@ -116,8 +116,39 @@ export function generateReactBindings(
|
|||||||
);
|
);
|
||||||
return { iface, view: `${reference} & {call: {${calls.join(";")}}}` };
|
return { iface, view: `${reference} & {call: {${calls.join(";")}}}` };
|
||||||
});
|
});
|
||||||
|
const queryType = (value: ValueType): string => {
|
||||||
|
if (value.kind === "object-ref")
|
||||||
|
return `QueryReference<${q(value.expectation.kind === "atom" ? value.expectation.atomId : value.expectation.interfaceRevisionId)}>`;
|
||||||
|
if (value.kind === "optional") return `(${queryType(value.value)} | null)`;
|
||||||
|
if (value.kind === "list") return `Array<${queryType(value.value)}>`;
|
||||||
|
if (value.kind === "record")
|
||||||
|
return `{${Object.entries(value.fields)
|
||||||
|
.map(([name, field]) => `${q(name)}: ${queryType(field)}`)
|
||||||
|
.join(";")}}`;
|
||||||
|
return type(value);
|
||||||
|
};
|
||||||
|
const queries = pkg.checkedQueries ?? [];
|
||||||
|
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)}: ${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)}]>`).join(";")}} = ${JSON.stringify(
|
||||||
|
Object.fromEntries(
|
||||||
|
queries.map((query) => [
|
||||||
|
query.declaration.displayName,
|
||||||
|
{
|
||||||
|
id: `${pkg.revisionId}:${query.declaration.id}`,
|
||||||
|
definitionDigest: query.definitionDigest,
|
||||||
|
rootInterfaceRevisionId: query.declaration.root,
|
||||||
|
variables: query.variables,
|
||||||
|
output: query.output,
|
||||||
|
watch: query.declaration.watch,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)};\n`;
|
||||||
return (
|
return (
|
||||||
`// Generated from checked QX contracts. Do not edit.\nimport {defineReactInterfaceContract} from "@quixos/web-studio-react-runtime";\nimport type {ObjectRef, InterfaceReference, ReadableField, WritableField} from "@quixos/web-studio-react-runtime";\n${declarations.join("\n")}\nexport type ReactResults = {${results.join(";\n")}};\n` +
|
`// Generated from checked QX contracts. Do not edit.\nimport {defineReactInterfaceContract} from "@quixos/web-studio-react-runtime";\nimport type {ObjectRef, InterfaceReference, ReadableField, WritableField, QueryDescriptor, QueryReference} from "@quixos/web-studio-react-runtime";\n${declarations.join("\n")}\nexport type ReactResults = {${results.join(";\n")}};\n` +
|
||||||
|
queryCode +
|
||||||
`const contractsData = ${JSON.stringify(schema.interfaces)};\n` +
|
`const contractsData = ${JSON.stringify(schema.interfaces)};\n` +
|
||||||
`export const reactContracts = {${contracts.map(({ iface, view }) => `${q(iface.revisionId)}: defineReactInterfaceContract<${view}>(${q(iface.revisionId)}, contractsData)`).join(",\n")}};\n` +
|
`export const reactContracts = {${contracts.map(({ iface, view }) => `${q(iface.revisionId)}: defineReactInterfaceContract<${view}>(${q(iface.revisionId)}, contractsData)`).join(",\n")}};\n` +
|
||||||
`export const reactInterfaces = {${contracts
|
`export const reactInterfaces = {${contracts
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { readFile, realpath } from "node:fs/promises";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { loadQxSources, resolveQxSources } from "./source-loader.js";
|
import { loadQxSources, resolveQxSources } from "./source-loader.js";
|
||||||
import { compileRepositoryQuery } from "../query/compile.js";
|
import { compileRepositoryQuery } from "../query/compile.js";
|
||||||
|
import { checkQueryTemplate } from "../query/templates.js";
|
||||||
|
import { readRepositorySource } from "./source-loader.js";
|
||||||
import { linkQueries } from "../query/link.js";
|
import { linkQueries } from "../query/link.js";
|
||||||
import {
|
import {
|
||||||
capabilityId,
|
capabilityId,
|
||||||
@@ -237,6 +239,10 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
|
|||||||
...(environment.interfaceClosure ?? []),
|
...(environment.interfaceClosure ?? []),
|
||||||
...(compiled.resource.specializations ?? []),
|
...(compiled.resource.specializations ?? []),
|
||||||
];
|
];
|
||||||
|
for (const template of compiled.resource.revision.queryTemplates ?? [])
|
||||||
|
await checkQueryTemplate(template, queryInterfaces, (name) =>
|
||||||
|
readRepositorySource(snapshot.directory, name),
|
||||||
|
);
|
||||||
if (compiled.resource.revision.queries?.length)
|
if (compiled.resource.revision.queries?.length)
|
||||||
compiled.resource.revision.checkedQueries = await Promise.all(
|
compiled.resource.revision.checkedQueries = await Promise.all(
|
||||||
compiled.resource.revision.queries.map((query) =>
|
compiled.resource.revision.queries.map((query) =>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,251 +1,253 @@
|
|||||||
WORKSPACE=1
|
WORKSPACE=1
|
||||||
QUERY=2
|
QUERY=2
|
||||||
ROOT=3
|
SPECIALIZE=3
|
||||||
DOCUMENT=4
|
ROOT=4
|
||||||
FRAGMENTS=5
|
DOCUMENT=5
|
||||||
VIEW=6
|
FRAGMENTS=6
|
||||||
MAX=7
|
VIEW=7
|
||||||
ALLOW=8
|
MAX=8
|
||||||
POLL=9
|
ALLOW=9
|
||||||
QUERYABLE=10
|
POLL=10
|
||||||
RPC=11
|
QUERYABLE=11
|
||||||
QUERY_REASON=12
|
RPC=12
|
||||||
TYPE=13
|
QUERY_REASON=13
|
||||||
OBJECT=14
|
TYPE=14
|
||||||
STORABLE=15
|
OBJECT=15
|
||||||
IMPLEMENTS=16
|
STORABLE=16
|
||||||
REF=17
|
IMPLEMENTS=17
|
||||||
FRAGMENT=18
|
REF=18
|
||||||
IMPORT=19
|
FRAGMENT=19
|
||||||
EXTERNAL=20
|
IMPORT=20
|
||||||
ATOM=21
|
EXTERNAL=21
|
||||||
INTERFACE=22
|
ATOM=22
|
||||||
INTERFACES=23
|
INTERFACE=23
|
||||||
PACKAGE=24
|
INTERFACES=24
|
||||||
VALUE=25
|
PACKAGE=25
|
||||||
RELATION=26
|
VALUE=26
|
||||||
OPERATION=27
|
RELATION=27
|
||||||
FUNCTION=28
|
OPERATION=28
|
||||||
CONSTRUCTOR=29
|
FUNCTION=29
|
||||||
CONSTRUCTS=30
|
CONSTRUCTOR=30
|
||||||
INPUT=31
|
CONSTRUCTS=31
|
||||||
CONFORM=32
|
INPUT=32
|
||||||
AS=33
|
CONFORM=33
|
||||||
BIND=34
|
AS=34
|
||||||
STATIC=35
|
BIND=35
|
||||||
TO=36
|
STATIC=36
|
||||||
PRIVATE=37
|
TO=37
|
||||||
SHARED=38
|
PRIVATE=38
|
||||||
STATE=39
|
SHARED=39
|
||||||
EDGE=40
|
STATE=40
|
||||||
PROJECTION=41
|
EDGE=41
|
||||||
WITH=42
|
PROJECTION=42
|
||||||
USING=43
|
WITH=43
|
||||||
VIA=44
|
USING=44
|
||||||
MATERIALIZE=45
|
VIA=45
|
||||||
IF=46
|
MATERIALIZE=46
|
||||||
ABSENT=47
|
IF=47
|
||||||
ON=48
|
ABSENT=48
|
||||||
POLICY=49
|
ON=49
|
||||||
DEFAULT=50
|
POLICY=50
|
||||||
SOURCE=51
|
DEFAULT=51
|
||||||
REPOSITORY=52
|
SOURCE=52
|
||||||
COMMIT=53
|
REPOSITORY=53
|
||||||
REVISION=54
|
COMMIT=54
|
||||||
SEMANTIC_MAJOR=55
|
REVISION=55
|
||||||
ON_DELETE=56
|
SEMANTIC_MAJOR=56
|
||||||
RETAIN_OTHER=57
|
ON_DELETE=57
|
||||||
KEYED=58
|
RETAIN_OTHER=58
|
||||||
PUBLIC_TRAVERSAL=59
|
KEYED=59
|
||||||
ID=60
|
PUBLIC_TRAVERSAL=60
|
||||||
DOC=61
|
ID=61
|
||||||
MODE=62
|
DOC=62
|
||||||
EMITS=63
|
MODE=63
|
||||||
RECEIVER=64
|
EMITS=64
|
||||||
REQUIRES=65
|
RECEIVER=65
|
||||||
ANY=66
|
REQUIRES=66
|
||||||
GET=67
|
ANY=67
|
||||||
SET=68
|
GET=68
|
||||||
WATCH=69
|
SET=69
|
||||||
START=70
|
WATCH=70
|
||||||
STOP=71
|
START=71
|
||||||
READ=72
|
STOP=72
|
||||||
WRITE=73
|
READ=73
|
||||||
RESOLVE=74
|
WRITE=74
|
||||||
CONNECT=75
|
RESOLVE=75
|
||||||
DISCONNECT=76
|
CONNECT=76
|
||||||
CALL=77
|
DISCONNECT=77
|
||||||
WATCH_START=78
|
CALL=78
|
||||||
WATCH_STOP=79
|
WATCH_START=79
|
||||||
SUBSCRIBE=80
|
WATCH_STOP=80
|
||||||
UNSUBSCRIBE=81
|
SUBSCRIBE=81
|
||||||
OPTIMISTIC_REGISTER=82
|
UNSUBSCRIBE=82
|
||||||
CRDT=83
|
OPTIMISTIC_REGISTER=83
|
||||||
OPTIONAL_ONE=84
|
CRDT=84
|
||||||
EXACTLY_ONE=85
|
OPTIONAL_ONE=85
|
||||||
MANY_UNIQUE=86
|
EXACTLY_ONE=86
|
||||||
MANY=87
|
MANY_UNIQUE=87
|
||||||
ORDERED=88
|
MANY=88
|
||||||
UNIT=89
|
ORDERED=89
|
||||||
WATCH_HANDLE=90
|
UNIT=90
|
||||||
MESSAGE=91
|
WATCH_HANDLE=91
|
||||||
ATOM_REF=92
|
MESSAGE=92
|
||||||
INTERFACE_REF=93
|
ATOM_REF=93
|
||||||
OPTIONAL=94
|
INTERFACE_REF=94
|
||||||
LIST=95
|
OPTIONAL=95
|
||||||
RECORD=96
|
LIST=96
|
||||||
BOOL=97
|
RECORD=97
|
||||||
BYTES=98
|
BOOL=98
|
||||||
DOUBLE=99
|
BYTES=99
|
||||||
INT32=100
|
DOUBLE=100
|
||||||
INT64=101
|
INT32=101
|
||||||
STRING=102
|
INT64=102
|
||||||
UINT32=103
|
STRING=103
|
||||||
UINT64=104
|
UINT32=104
|
||||||
TRUE=105
|
UINT64=105
|
||||||
FALSE=106
|
TRUE=106
|
||||||
NULL=107
|
FALSE=107
|
||||||
ARROW=108
|
NULL=108
|
||||||
COLON=109
|
ARROW=109
|
||||||
SEMI=110
|
COLON=110
|
||||||
COMMA=111
|
SEMI=111
|
||||||
DOT=112
|
COMMA=112
|
||||||
LBRACE=113
|
DOT=113
|
||||||
RBRACE=114
|
LBRACE=114
|
||||||
LBRACK=115
|
RBRACE=115
|
||||||
RBRACK=116
|
LBRACK=116
|
||||||
LPAREN=117
|
RBRACK=117
|
||||||
RPAREN=118
|
LPAREN=118
|
||||||
LT=119
|
RPAREN=119
|
||||||
GT=120
|
LT=120
|
||||||
AMP=121
|
GT=121
|
||||||
EQUAL=122
|
AMP=122
|
||||||
INTEGER=123
|
EQUAL=123
|
||||||
JSON_NUMBER=124
|
INTEGER=124
|
||||||
IDENTIFIER=125
|
JSON_NUMBER=125
|
||||||
STRING_LITERAL=126
|
IDENTIFIER=126
|
||||||
LINE_COMMENT=127
|
STRING_LITERAL=127
|
||||||
BLOCK_COMMENT=128
|
LINE_COMMENT=128
|
||||||
WS=129
|
BLOCK_COMMENT=129
|
||||||
|
WS=130
|
||||||
'workspace'=1
|
'workspace'=1
|
||||||
'query'=2
|
'query'=2
|
||||||
'root'=3
|
'specialize'=3
|
||||||
'document'=4
|
'root'=4
|
||||||
'fragments'=5
|
'document'=5
|
||||||
'view'=6
|
'fragments'=6
|
||||||
'max'=7
|
'view'=7
|
||||||
'allow'=8
|
'max'=8
|
||||||
'poll'=9
|
'allow'=9
|
||||||
'queryable'=10
|
'poll'=10
|
||||||
'rpc'=11
|
'queryable'=11
|
||||||
'query-reason'=12
|
'rpc'=12
|
||||||
'type'=13
|
'query-reason'=13
|
||||||
'object'=14
|
'type'=14
|
||||||
'storable'=15
|
'object'=15
|
||||||
'implements'=16
|
'storable'=16
|
||||||
'ref'=17
|
'implements'=17
|
||||||
'fragment'=18
|
'ref'=18
|
||||||
'import'=19
|
'fragment'=19
|
||||||
'external'=20
|
'import'=20
|
||||||
'atom'=21
|
'external'=21
|
||||||
'interface'=22
|
'atom'=22
|
||||||
'interfaces'=23
|
'interface'=23
|
||||||
'package'=24
|
'interfaces'=24
|
||||||
'value'=25
|
'package'=25
|
||||||
'relation'=26
|
'value'=26
|
||||||
'operation'=27
|
'relation'=27
|
||||||
'function'=28
|
'operation'=28
|
||||||
'constructor'=29
|
'function'=29
|
||||||
'constructs'=30
|
'constructor'=30
|
||||||
'input'=31
|
'constructs'=31
|
||||||
'conform'=32
|
'input'=32
|
||||||
'as'=33
|
'conform'=33
|
||||||
'bind'=34
|
'as'=34
|
||||||
'static'=35
|
'bind'=35
|
||||||
'to'=36
|
'static'=36
|
||||||
'private'=37
|
'to'=37
|
||||||
'shared'=38
|
'private'=38
|
||||||
'state'=39
|
'shared'=39
|
||||||
'edge'=40
|
'state'=40
|
||||||
'projection'=41
|
'edge'=41
|
||||||
'with'=42
|
'projection'=42
|
||||||
'using'=43
|
'with'=43
|
||||||
'via'=44
|
'using'=44
|
||||||
'materialize'=45
|
'via'=45
|
||||||
'if'=46
|
'materialize'=46
|
||||||
'absent'=47
|
'if'=47
|
||||||
'on'=48
|
'absent'=48
|
||||||
'policy'=49
|
'on'=49
|
||||||
'default'=50
|
'policy'=50
|
||||||
'source'=51
|
'default'=51
|
||||||
'repository'=52
|
'source'=52
|
||||||
'commit'=53
|
'repository'=53
|
||||||
'revision'=54
|
'commit'=54
|
||||||
'semantic-major'=55
|
'revision'=55
|
||||||
'on-delete'=56
|
'semantic-major'=56
|
||||||
'retain-other'=57
|
'on-delete'=57
|
||||||
'keyed'=58
|
'retain-other'=58
|
||||||
'public-traversal'=59
|
'keyed'=59
|
||||||
'id'=60
|
'public-traversal'=60
|
||||||
'doc'=61
|
'id'=61
|
||||||
'mode'=62
|
'doc'=62
|
||||||
'emits'=63
|
'mode'=63
|
||||||
'receiver'=64
|
'emits'=64
|
||||||
'requires'=65
|
'receiver'=65
|
||||||
'any'=66
|
'requires'=66
|
||||||
'get'=67
|
'any'=67
|
||||||
'set'=68
|
'get'=68
|
||||||
'watch'=69
|
'set'=69
|
||||||
'start'=70
|
'watch'=70
|
||||||
'stop'=71
|
'start'=71
|
||||||
'read'=72
|
'stop'=72
|
||||||
'write'=73
|
'read'=73
|
||||||
'resolve'=74
|
'write'=74
|
||||||
'connect'=75
|
'resolve'=75
|
||||||
'disconnect'=76
|
'connect'=76
|
||||||
'call'=77
|
'disconnect'=77
|
||||||
'watch-start'=78
|
'call'=78
|
||||||
'watch-stop'=79
|
'watch-start'=79
|
||||||
'subscribe'=80
|
'watch-stop'=80
|
||||||
'unsubscribe'=81
|
'subscribe'=81
|
||||||
'optimistic-register'=82
|
'unsubscribe'=82
|
||||||
'crdt'=83
|
'optimistic-register'=83
|
||||||
'optional-one'=84
|
'crdt'=84
|
||||||
'exactly-one'=85
|
'optional-one'=85
|
||||||
'many-unique'=86
|
'exactly-one'=86
|
||||||
'many'=87
|
'many-unique'=87
|
||||||
'ordered'=88
|
'many'=88
|
||||||
'unit'=89
|
'ordered'=89
|
||||||
'watch-handle'=90
|
'unit'=90
|
||||||
'message'=91
|
'watch-handle'=91
|
||||||
'atom-ref'=92
|
'message'=92
|
||||||
'interface-ref'=93
|
'atom-ref'=93
|
||||||
'optional'=94
|
'interface-ref'=94
|
||||||
'list'=95
|
'optional'=95
|
||||||
'record'=96
|
'list'=96
|
||||||
'bool'=97
|
'record'=97
|
||||||
'bytes'=98
|
'bool'=98
|
||||||
'double'=99
|
'bytes'=99
|
||||||
'int32'=100
|
'double'=100
|
||||||
'int64'=101
|
'int32'=101
|
||||||
'string'=102
|
'int64'=102
|
||||||
'uint32'=103
|
'string'=103
|
||||||
'uint64'=104
|
'uint32'=104
|
||||||
'true'=105
|
'uint64'=105
|
||||||
'false'=106
|
'true'=106
|
||||||
'null'=107
|
'false'=107
|
||||||
'->'=108
|
'null'=108
|
||||||
':'=109
|
'->'=109
|
||||||
';'=110
|
':'=110
|
||||||
','=111
|
';'=111
|
||||||
'.'=112
|
','=112
|
||||||
'{'=113
|
'.'=113
|
||||||
'}'=114
|
'{'=114
|
||||||
'['=115
|
'}'=115
|
||||||
']'=116
|
'['=116
|
||||||
'('=117
|
']'=117
|
||||||
')'=118
|
'('=118
|
||||||
'<'=119
|
')'=119
|
||||||
'>'=120
|
'<'=120
|
||||||
'&'=121
|
'>'=121
|
||||||
'='=122
|
'&'=122
|
||||||
|
'='=123
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,251 +1,253 @@
|
|||||||
WORKSPACE=1
|
WORKSPACE=1
|
||||||
QUERY=2
|
QUERY=2
|
||||||
ROOT=3
|
SPECIALIZE=3
|
||||||
DOCUMENT=4
|
ROOT=4
|
||||||
FRAGMENTS=5
|
DOCUMENT=5
|
||||||
VIEW=6
|
FRAGMENTS=6
|
||||||
MAX=7
|
VIEW=7
|
||||||
ALLOW=8
|
MAX=8
|
||||||
POLL=9
|
ALLOW=9
|
||||||
QUERYABLE=10
|
POLL=10
|
||||||
RPC=11
|
QUERYABLE=11
|
||||||
QUERY_REASON=12
|
RPC=12
|
||||||
TYPE=13
|
QUERY_REASON=13
|
||||||
OBJECT=14
|
TYPE=14
|
||||||
STORABLE=15
|
OBJECT=15
|
||||||
IMPLEMENTS=16
|
STORABLE=16
|
||||||
REF=17
|
IMPLEMENTS=17
|
||||||
FRAGMENT=18
|
REF=18
|
||||||
IMPORT=19
|
FRAGMENT=19
|
||||||
EXTERNAL=20
|
IMPORT=20
|
||||||
ATOM=21
|
EXTERNAL=21
|
||||||
INTERFACE=22
|
ATOM=22
|
||||||
INTERFACES=23
|
INTERFACE=23
|
||||||
PACKAGE=24
|
INTERFACES=24
|
||||||
VALUE=25
|
PACKAGE=25
|
||||||
RELATION=26
|
VALUE=26
|
||||||
OPERATION=27
|
RELATION=27
|
||||||
FUNCTION=28
|
OPERATION=28
|
||||||
CONSTRUCTOR=29
|
FUNCTION=29
|
||||||
CONSTRUCTS=30
|
CONSTRUCTOR=30
|
||||||
INPUT=31
|
CONSTRUCTS=31
|
||||||
CONFORM=32
|
INPUT=32
|
||||||
AS=33
|
CONFORM=33
|
||||||
BIND=34
|
AS=34
|
||||||
STATIC=35
|
BIND=35
|
||||||
TO=36
|
STATIC=36
|
||||||
PRIVATE=37
|
TO=37
|
||||||
SHARED=38
|
PRIVATE=38
|
||||||
STATE=39
|
SHARED=39
|
||||||
EDGE=40
|
STATE=40
|
||||||
PROJECTION=41
|
EDGE=41
|
||||||
WITH=42
|
PROJECTION=42
|
||||||
USING=43
|
WITH=43
|
||||||
VIA=44
|
USING=44
|
||||||
MATERIALIZE=45
|
VIA=45
|
||||||
IF=46
|
MATERIALIZE=46
|
||||||
ABSENT=47
|
IF=47
|
||||||
ON=48
|
ABSENT=48
|
||||||
POLICY=49
|
ON=49
|
||||||
DEFAULT=50
|
POLICY=50
|
||||||
SOURCE=51
|
DEFAULT=51
|
||||||
REPOSITORY=52
|
SOURCE=52
|
||||||
COMMIT=53
|
REPOSITORY=53
|
||||||
REVISION=54
|
COMMIT=54
|
||||||
SEMANTIC_MAJOR=55
|
REVISION=55
|
||||||
ON_DELETE=56
|
SEMANTIC_MAJOR=56
|
||||||
RETAIN_OTHER=57
|
ON_DELETE=57
|
||||||
KEYED=58
|
RETAIN_OTHER=58
|
||||||
PUBLIC_TRAVERSAL=59
|
KEYED=59
|
||||||
ID=60
|
PUBLIC_TRAVERSAL=60
|
||||||
DOC=61
|
ID=61
|
||||||
MODE=62
|
DOC=62
|
||||||
EMITS=63
|
MODE=63
|
||||||
RECEIVER=64
|
EMITS=64
|
||||||
REQUIRES=65
|
RECEIVER=65
|
||||||
ANY=66
|
REQUIRES=66
|
||||||
GET=67
|
ANY=67
|
||||||
SET=68
|
GET=68
|
||||||
WATCH=69
|
SET=69
|
||||||
START=70
|
WATCH=70
|
||||||
STOP=71
|
START=71
|
||||||
READ=72
|
STOP=72
|
||||||
WRITE=73
|
READ=73
|
||||||
RESOLVE=74
|
WRITE=74
|
||||||
CONNECT=75
|
RESOLVE=75
|
||||||
DISCONNECT=76
|
CONNECT=76
|
||||||
CALL=77
|
DISCONNECT=77
|
||||||
WATCH_START=78
|
CALL=78
|
||||||
WATCH_STOP=79
|
WATCH_START=79
|
||||||
SUBSCRIBE=80
|
WATCH_STOP=80
|
||||||
UNSUBSCRIBE=81
|
SUBSCRIBE=81
|
||||||
OPTIMISTIC_REGISTER=82
|
UNSUBSCRIBE=82
|
||||||
CRDT=83
|
OPTIMISTIC_REGISTER=83
|
||||||
OPTIONAL_ONE=84
|
CRDT=84
|
||||||
EXACTLY_ONE=85
|
OPTIONAL_ONE=85
|
||||||
MANY_UNIQUE=86
|
EXACTLY_ONE=86
|
||||||
MANY=87
|
MANY_UNIQUE=87
|
||||||
ORDERED=88
|
MANY=88
|
||||||
UNIT=89
|
ORDERED=89
|
||||||
WATCH_HANDLE=90
|
UNIT=90
|
||||||
MESSAGE=91
|
WATCH_HANDLE=91
|
||||||
ATOM_REF=92
|
MESSAGE=92
|
||||||
INTERFACE_REF=93
|
ATOM_REF=93
|
||||||
OPTIONAL=94
|
INTERFACE_REF=94
|
||||||
LIST=95
|
OPTIONAL=95
|
||||||
RECORD=96
|
LIST=96
|
||||||
BOOL=97
|
RECORD=97
|
||||||
BYTES=98
|
BOOL=98
|
||||||
DOUBLE=99
|
BYTES=99
|
||||||
INT32=100
|
DOUBLE=100
|
||||||
INT64=101
|
INT32=101
|
||||||
STRING=102
|
INT64=102
|
||||||
UINT32=103
|
STRING=103
|
||||||
UINT64=104
|
UINT32=104
|
||||||
TRUE=105
|
UINT64=105
|
||||||
FALSE=106
|
TRUE=106
|
||||||
NULL=107
|
FALSE=107
|
||||||
ARROW=108
|
NULL=108
|
||||||
COLON=109
|
ARROW=109
|
||||||
SEMI=110
|
COLON=110
|
||||||
COMMA=111
|
SEMI=111
|
||||||
DOT=112
|
COMMA=112
|
||||||
LBRACE=113
|
DOT=113
|
||||||
RBRACE=114
|
LBRACE=114
|
||||||
LBRACK=115
|
RBRACE=115
|
||||||
RBRACK=116
|
LBRACK=116
|
||||||
LPAREN=117
|
RBRACK=117
|
||||||
RPAREN=118
|
LPAREN=118
|
||||||
LT=119
|
RPAREN=119
|
||||||
GT=120
|
LT=120
|
||||||
AMP=121
|
GT=121
|
||||||
EQUAL=122
|
AMP=122
|
||||||
INTEGER=123
|
EQUAL=123
|
||||||
JSON_NUMBER=124
|
INTEGER=124
|
||||||
IDENTIFIER=125
|
JSON_NUMBER=125
|
||||||
STRING_LITERAL=126
|
IDENTIFIER=126
|
||||||
LINE_COMMENT=127
|
STRING_LITERAL=127
|
||||||
BLOCK_COMMENT=128
|
LINE_COMMENT=128
|
||||||
WS=129
|
BLOCK_COMMENT=129
|
||||||
|
WS=130
|
||||||
'workspace'=1
|
'workspace'=1
|
||||||
'query'=2
|
'query'=2
|
||||||
'root'=3
|
'specialize'=3
|
||||||
'document'=4
|
'root'=4
|
||||||
'fragments'=5
|
'document'=5
|
||||||
'view'=6
|
'fragments'=6
|
||||||
'max'=7
|
'view'=7
|
||||||
'allow'=8
|
'max'=8
|
||||||
'poll'=9
|
'allow'=9
|
||||||
'queryable'=10
|
'poll'=10
|
||||||
'rpc'=11
|
'queryable'=11
|
||||||
'query-reason'=12
|
'rpc'=12
|
||||||
'type'=13
|
'query-reason'=13
|
||||||
'object'=14
|
'type'=14
|
||||||
'storable'=15
|
'object'=15
|
||||||
'implements'=16
|
'storable'=16
|
||||||
'ref'=17
|
'implements'=17
|
||||||
'fragment'=18
|
'ref'=18
|
||||||
'import'=19
|
'fragment'=19
|
||||||
'external'=20
|
'import'=20
|
||||||
'atom'=21
|
'external'=21
|
||||||
'interface'=22
|
'atom'=22
|
||||||
'interfaces'=23
|
'interface'=23
|
||||||
'package'=24
|
'interfaces'=24
|
||||||
'value'=25
|
'package'=25
|
||||||
'relation'=26
|
'value'=26
|
||||||
'operation'=27
|
'relation'=27
|
||||||
'function'=28
|
'operation'=28
|
||||||
'constructor'=29
|
'function'=29
|
||||||
'constructs'=30
|
'constructor'=30
|
||||||
'input'=31
|
'constructs'=31
|
||||||
'conform'=32
|
'input'=32
|
||||||
'as'=33
|
'conform'=33
|
||||||
'bind'=34
|
'as'=34
|
||||||
'static'=35
|
'bind'=35
|
||||||
'to'=36
|
'static'=36
|
||||||
'private'=37
|
'to'=37
|
||||||
'shared'=38
|
'private'=38
|
||||||
'state'=39
|
'shared'=39
|
||||||
'edge'=40
|
'state'=40
|
||||||
'projection'=41
|
'edge'=41
|
||||||
'with'=42
|
'projection'=42
|
||||||
'using'=43
|
'with'=43
|
||||||
'via'=44
|
'using'=44
|
||||||
'materialize'=45
|
'via'=45
|
||||||
'if'=46
|
'materialize'=46
|
||||||
'absent'=47
|
'if'=47
|
||||||
'on'=48
|
'absent'=48
|
||||||
'policy'=49
|
'on'=49
|
||||||
'default'=50
|
'policy'=50
|
||||||
'source'=51
|
'default'=51
|
||||||
'repository'=52
|
'source'=52
|
||||||
'commit'=53
|
'repository'=53
|
||||||
'revision'=54
|
'commit'=54
|
||||||
'semantic-major'=55
|
'revision'=55
|
||||||
'on-delete'=56
|
'semantic-major'=56
|
||||||
'retain-other'=57
|
'on-delete'=57
|
||||||
'keyed'=58
|
'retain-other'=58
|
||||||
'public-traversal'=59
|
'keyed'=59
|
||||||
'id'=60
|
'public-traversal'=60
|
||||||
'doc'=61
|
'id'=61
|
||||||
'mode'=62
|
'doc'=62
|
||||||
'emits'=63
|
'mode'=63
|
||||||
'receiver'=64
|
'emits'=64
|
||||||
'requires'=65
|
'receiver'=65
|
||||||
'any'=66
|
'requires'=66
|
||||||
'get'=67
|
'any'=67
|
||||||
'set'=68
|
'get'=68
|
||||||
'watch'=69
|
'set'=69
|
||||||
'start'=70
|
'watch'=70
|
||||||
'stop'=71
|
'start'=71
|
||||||
'read'=72
|
'stop'=72
|
||||||
'write'=73
|
'read'=73
|
||||||
'resolve'=74
|
'write'=74
|
||||||
'connect'=75
|
'resolve'=75
|
||||||
'disconnect'=76
|
'connect'=76
|
||||||
'call'=77
|
'disconnect'=77
|
||||||
'watch-start'=78
|
'call'=78
|
||||||
'watch-stop'=79
|
'watch-start'=79
|
||||||
'subscribe'=80
|
'watch-stop'=80
|
||||||
'unsubscribe'=81
|
'subscribe'=81
|
||||||
'optimistic-register'=82
|
'unsubscribe'=82
|
||||||
'crdt'=83
|
'optimistic-register'=83
|
||||||
'optional-one'=84
|
'crdt'=84
|
||||||
'exactly-one'=85
|
'optional-one'=85
|
||||||
'many-unique'=86
|
'exactly-one'=86
|
||||||
'many'=87
|
'many-unique'=87
|
||||||
'ordered'=88
|
'many'=88
|
||||||
'unit'=89
|
'ordered'=89
|
||||||
'watch-handle'=90
|
'unit'=90
|
||||||
'message'=91
|
'watch-handle'=91
|
||||||
'atom-ref'=92
|
'message'=92
|
||||||
'interface-ref'=93
|
'atom-ref'=93
|
||||||
'optional'=94
|
'interface-ref'=94
|
||||||
'list'=95
|
'optional'=95
|
||||||
'record'=96
|
'list'=96
|
||||||
'bool'=97
|
'record'=97
|
||||||
'bytes'=98
|
'bool'=98
|
||||||
'double'=99
|
'bytes'=99
|
||||||
'int32'=100
|
'double'=100
|
||||||
'int64'=101
|
'int32'=101
|
||||||
'string'=102
|
'int64'=102
|
||||||
'uint32'=103
|
'string'=103
|
||||||
'uint64'=104
|
'uint32'=104
|
||||||
'true'=105
|
'uint64'=105
|
||||||
'false'=106
|
'true'=106
|
||||||
'null'=107
|
'false'=107
|
||||||
'->'=108
|
'null'=108
|
||||||
':'=109
|
'->'=109
|
||||||
';'=110
|
':'=110
|
||||||
','=111
|
';'=111
|
||||||
'.'=112
|
','=112
|
||||||
'{'=113
|
'.'=113
|
||||||
'}'=114
|
'{'=114
|
||||||
'['=115
|
'}'=115
|
||||||
']'=116
|
'['=116
|
||||||
'('=117
|
']'=117
|
||||||
')'=118
|
'('=118
|
||||||
'<'=119
|
')'=119
|
||||||
'>'=120
|
'<'=120
|
||||||
'&'=121
|
'>'=121
|
||||||
'='=122
|
'&'=122
|
||||||
|
'='=123
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@ import { TargetConstraintContext } from "./QuixosCapabilityParser.js";
|
|||||||
import { PackageResourceDeclContext } from "./QuixosCapabilityParser.js";
|
import { PackageResourceDeclContext } from "./QuixosCapabilityParser.js";
|
||||||
import { PackageExportContext } from "./QuixosCapabilityParser.js";
|
import { PackageExportContext } from "./QuixosCapabilityParser.js";
|
||||||
import { PackageQueryContext } from "./QuixosCapabilityParser.js";
|
import { PackageQueryContext } from "./QuixosCapabilityParser.js";
|
||||||
|
import { PackageQuerySpecializationContext } from "./QuixosCapabilityParser.js";
|
||||||
import { QueryClauseContext } from "./QuixosCapabilityParser.js";
|
import { QueryClauseContext } from "./QuixosCapabilityParser.js";
|
||||||
import { PackageOperationExportContext } from "./QuixosCapabilityParser.js";
|
import { PackageOperationExportContext } from "./QuixosCapabilityParser.js";
|
||||||
import { PackageFunctionExportContext } from "./QuixosCapabilityParser.js";
|
import { PackageFunctionExportContext } from "./QuixosCapabilityParser.js";
|
||||||
@@ -249,6 +250,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
|
|||||||
* @return the visitor result
|
* @return the visitor result
|
||||||
*/
|
*/
|
||||||
visitPackageQuery?: (ctx: PackageQueryContext) => Result;
|
visitPackageQuery?: (ctx: PackageQueryContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `QuixosCapabilityParser.packageQuerySpecialization`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitPackageQuerySpecialization?: (ctx: PackageQuerySpecializationContext) => Result;
|
||||||
/**
|
/**
|
||||||
* Visit a parse tree produced by `QuixosCapabilityParser.queryClause`.
|
* Visit a parse tree produced by `QuixosCapabilityParser.queryClause`.
|
||||||
* @param ctx the parse tree
|
* @param ctx the parse tree
|
||||||
|
|||||||
@@ -46,7 +46,14 @@ import {
|
|||||||
specializePackageExport,
|
specializePackageExport,
|
||||||
} from "../capability-model/index.js";
|
} from "../capability-model/index.js";
|
||||||
import { GenericSourceTypes } from "./generic-types.js";
|
import { GenericSourceTypes } from "./generic-types.js";
|
||||||
import { defaultQueryBudgets, type QueryDeclaration, type QueryBudgets, type QueryUse } from "../query/types.js";
|
import {
|
||||||
|
defaultQueryBudgets,
|
||||||
|
type QueryDeclaration,
|
||||||
|
type QueryBudgets,
|
||||||
|
type QueryUse,
|
||||||
|
type QueryTemplate,
|
||||||
|
} from "../query/types.js";
|
||||||
|
import { specializeQueryTemplate } from "../query/templates.js";
|
||||||
import { QuixosCapabilityLexer } from "./generated/QuixosCapabilityLexer.js";
|
import { QuixosCapabilityLexer } from "./generated/QuixosCapabilityLexer.js";
|
||||||
import {
|
import {
|
||||||
QuixosCapabilityParser,
|
QuixosCapabilityParser,
|
||||||
@@ -177,6 +184,7 @@ interface PackageExportSymbol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface PackageSymbol {
|
interface PackageSymbol {
|
||||||
|
queries?: Map<string, string>;
|
||||||
definition?: PackageRevision;
|
definition?: PackageRevision;
|
||||||
revisionId: PackageRevision["revisionId"];
|
revisionId: PackageRevision["revisionId"];
|
||||||
exports: Map<string, PackageExportSymbol>;
|
exports: Map<string, PackageExportSymbol>;
|
||||||
@@ -535,6 +543,7 @@ const lowerRelationshipMember = (
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
kind: "relationship",
|
kind: "relationship",
|
||||||
|
...(context.KEYED() ? { keyType: stringValue(context.stringLiteral(1)) as "string" | "boolean" | "int64" } : {}),
|
||||||
...(context.queryReadContract()
|
...(context.queryReadContract()
|
||||||
? {
|
? {
|
||||||
queryRead: {
|
queryRead: {
|
||||||
@@ -542,7 +551,7 @@ const lowerRelationshipMember = (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
id: capabilityId.member(stringValue(context.stringLiteral())),
|
id: capabilityId.member(stringValue(context.stringLiteral(0))),
|
||||||
displayName: identifier(context.identifier()),
|
displayName: identifier(context.identifier()),
|
||||||
target,
|
target,
|
||||||
cardinality,
|
cardinality,
|
||||||
@@ -747,6 +756,9 @@ const lowerInterfaceTemplate = (
|
|||||||
: { kind: cardinality === "optional-one" ? "optional" : "list", value: targetType };
|
: { kind: cardinality === "optional-one" ? "optional" : "list", value: targetType };
|
||||||
return {
|
return {
|
||||||
kind: "relationship",
|
kind: "relationship",
|
||||||
|
...(relationship.KEYED()
|
||||||
|
? { keyType: stringValue(relationship.stringLiteral(1)) as "string" | "boolean" | "int64" }
|
||||||
|
: {}),
|
||||||
...(relationship.queryReadContract()
|
...(relationship.queryReadContract()
|
||||||
? {
|
? {
|
||||||
queryRead: {
|
queryRead: {
|
||||||
@@ -754,7 +766,7 @@ const lowerInterfaceTemplate = (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
id: capabilityId.member(stringValue(relationship.stringLiteral())),
|
id: capabilityId.member(stringValue(relationship.stringLiteral(0))),
|
||||||
displayName: identifier(relationship.identifier()),
|
displayName: identifier(relationship.identifier()),
|
||||||
target,
|
target,
|
||||||
cardinality,
|
cardinality,
|
||||||
@@ -818,9 +830,34 @@ const lowerInterfaceTemplate = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const lowerQueryReference = (
|
||||||
|
state: LoweringState,
|
||||||
|
packageName: string,
|
||||||
|
queryName: string,
|
||||||
|
context: ParserRuleContext,
|
||||||
|
) => {
|
||||||
|
const pkg = requireSymbol(state, state.packages, packageName, context, "package");
|
||||||
|
const queryId =
|
||||||
|
pkg?.queries?.get(queryName) ?? pkg?.definition?.queries?.find((query) => query.displayName === queryName)?.id;
|
||||||
|
if (!pkg || !queryId) {
|
||||||
|
loweringIssue(state, context, "unknown-query", `Unknown query export ${packageName}.${queryName}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return { packageRevisionId: pkg.revisionId, queryId };
|
||||||
|
};
|
||||||
|
|
||||||
const lowerDependencyPort = (state: LoweringState, context: DependencyPortContext): DependencyPort | undefined => {
|
const lowerDependencyPort = (state: LoweringState, context: DependencyPortContext): DependencyPort | undefined => {
|
||||||
const name = identifier(context.identifier(0));
|
const name = identifier(context.identifier(0));
|
||||||
const id = capabilityId.dependencyPort(stringValue(context.stringLiteral()));
|
const id = capabilityId.dependencyPort(stringValue(context.stringLiteral()));
|
||||||
|
if (context.QUERY()) {
|
||||||
|
const query = lowerQueryReference(
|
||||||
|
state,
|
||||||
|
identifier(context.identifier(1)),
|
||||||
|
identifier(context.identifier(2)),
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
return query ? { id, displayName: name, requirement: { kind: "query", ...query } } : undefined;
|
||||||
|
}
|
||||||
if (context.STATE()) {
|
if (context.STATE()) {
|
||||||
const primitives = context
|
const primitives = context
|
||||||
.primitiveList()!
|
.primitiveList()!
|
||||||
@@ -1060,6 +1097,16 @@ const lowerGenericPackageExport = (
|
|||||||
.primitiveList()
|
.primitiveList()
|
||||||
?.primitive()
|
?.primitive()
|
||||||
.map((item) => text(item)) ?? [];
|
.map((item) => text(item)) ?? [];
|
||||||
|
if (port.QUERY()) {
|
||||||
|
const query = lowerQueryReference(state, identifier(port.identifier(1)), identifier(port.identifier(2)), port);
|
||||||
|
if (!query)
|
||||||
|
throw new GenericTypeError(
|
||||||
|
"unknown-query",
|
||||||
|
base.displayName,
|
||||||
|
"Query port requires an existing closed query export",
|
||||||
|
);
|
||||||
|
return { ...base, requirement: { kind: "query", ...query } };
|
||||||
|
}
|
||||||
if (port.STATE())
|
if (port.STATE())
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
@@ -1158,91 +1205,178 @@ const lowerPackage = (
|
|||||||
source: SourceRevision,
|
source: SourceRevision,
|
||||||
): PackageRevision => {
|
): PackageRevision => {
|
||||||
const alias = identifier(context.identifier());
|
const alias = identifier(context.identifier());
|
||||||
|
state.packages.get(alias)!.queries = new Map(
|
||||||
|
context.packageExport().flatMap((entry) => {
|
||||||
|
const query = entry.packageQuery();
|
||||||
|
const specialization = entry.packageQuerySpecialization();
|
||||||
|
return query && !query.typeParameters()
|
||||||
|
? [[identifier(query.identifier()), stringValue(query.stringLiteral(0))] as const]
|
||||||
|
: specialization
|
||||||
|
? [[identifier(specialization.identifier(0)), stringValue(specialization.stringLiteral())] as const]
|
||||||
|
: [];
|
||||||
|
}),
|
||||||
|
);
|
||||||
const genericExports: GenericPackageExport[] = [];
|
const genericExports: GenericPackageExport[] = [];
|
||||||
const queries: QueryDeclaration[] = [];
|
const queries: QueryDeclaration[] = [];
|
||||||
|
const queryTemplates: QueryTemplate[] = [];
|
||||||
const exports = context.packageExport().flatMap((exportContext): PackageExport[] => {
|
const exports = context.packageExport().flatMap((exportContext): PackageExport[] => {
|
||||||
|
if (exportContext.packageQuerySpecialization()) return [];
|
||||||
const query = exportContext.packageQuery();
|
const query = exportContext.packageQuery();
|
||||||
if (query) {
|
if (query) {
|
||||||
const close = (ctx: import("./generated/QuixosCapabilityParser.js").InterfaceTypeContext) =>
|
const previousParameters = state.types.parameters;
|
||||||
new TypeSubstitution(state.types.environment()).application(state.types.interface(ctx));
|
state.types.parameters = new Map();
|
||||||
const declaration: QueryDeclaration = {
|
try {
|
||||||
id: stringValue(query.stringLiteral(0)),
|
const parameters = state.types.declareParameters(
|
||||||
displayName: identifier(query.identifier()),
|
query.typeParameters(),
|
||||||
root: close(query.interfaceType()),
|
`query:${stringValue(query.stringLiteral(0))}`,
|
||||||
document: stringValue(query.stringLiteral(1)),
|
);
|
||||||
operation: stringValue(query.stringLiteral(2)),
|
const expressions = new Map<string, ReturnType<GenericSourceTypes["interface"]>>();
|
||||||
fragments: [],
|
const templateViews: QueryTemplate["views"] = [];
|
||||||
views: [],
|
const templateAllowances: QueryTemplate["allowances"] = [];
|
||||||
allowances: [],
|
const close = (ctx: import("./generated/QuixosCapabilityParser.js").InterfaceTypeContext) =>
|
||||||
budgets: { ...defaultQueryBudgets },
|
parameters.length
|
||||||
watch: false,
|
? (() => {
|
||||||
};
|
const id = capabilityId.interfaceRevision(`query-expression:${expressions.size}`);
|
||||||
const budgetNames: Record<string, keyof QueryBudgets> = {
|
expressions.set(id, state.types.interface(ctx));
|
||||||
rows: "rows",
|
return id;
|
||||||
depth: "depth",
|
})()
|
||||||
"result-bytes": "resultBytes",
|
: new TypeSubstitution(state.types.environment()).application(state.types.interface(ctx));
|
||||||
candidates: "candidates",
|
const declaration: QueryDeclaration = {
|
||||||
"rpc-calls": "rpcCalls",
|
id: stringValue(query.stringLiteral(0)),
|
||||||
concurrency: "concurrency",
|
displayName: identifier(query.identifier()),
|
||||||
"deadline-ms": "deadlineMs",
|
root: close(query.interfaceType()),
|
||||||
};
|
document: stringValue(query.stringLiteral(1)),
|
||||||
const assigned = new Set<string>();
|
operation: stringValue(query.stringLiteral(2)),
|
||||||
for (const clause of query.queryClause()) {
|
fragments: [],
|
||||||
if (clause.FRAGMENTS()) declaration.fragments.push(stringValue(clause.stringLiteral()!));
|
views: [],
|
||||||
else if (clause.VIEW()) {
|
allowances: [],
|
||||||
const name = identifier(clause.identifier(0));
|
budgets: { ...defaultQueryBudgets },
|
||||||
const atomId = requireSymbol(state, state.atoms, name, clause, "atom");
|
watch: false,
|
||||||
if (atomId) declaration.views.push({ atomId, interfaceRevisionId: close(clause.interfaceType()!) });
|
};
|
||||||
} else if (clause.MAX()) {
|
const budgetNames: Record<string, keyof QueryBudgets> = {
|
||||||
const name = identifier(clause.identifier(0));
|
rows: "rows",
|
||||||
const key = budgetNames[name],
|
depth: "depth",
|
||||||
n = Number(clause.INTEGER()!.getText());
|
"result-bytes": "resultBytes",
|
||||||
if (!key || assigned.has(name) || !Number.isSafeInteger(n) || n < 1 || n > defaultQueryBudgets[key])
|
candidates: "candidates",
|
||||||
loweringIssue(
|
"rpc-calls": "rpcCalls",
|
||||||
state,
|
concurrency: "concurrency",
|
||||||
clause,
|
"deadline-ms": "deadlineMs",
|
||||||
"invalid-query-budget",
|
};
|
||||||
`Invalid, duplicate, or excessive query budget ${name}`,
|
const assigned = new Set<string>();
|
||||||
|
for (const clause of query.queryClause()) {
|
||||||
|
if (clause.FRAGMENTS()) declaration.fragments.push(stringValue(clause.stringLiteral()!));
|
||||||
|
else if (clause.VIEW()) {
|
||||||
|
const name = identifier(clause.identifier(0));
|
||||||
|
if (parameters.length) {
|
||||||
|
const parameter = state.types.parameters.get(name);
|
||||||
|
const atomId = state.atoms.get(name);
|
||||||
|
if (clause.OBJECT() && parameter?.kind === "object")
|
||||||
|
templateViews.push({
|
||||||
|
target: { kind: "parameter", parameterId: parameter.id },
|
||||||
|
interface: state.types.interface(clause.interfaceType()!),
|
||||||
|
});
|
||||||
|
else if (!clause.OBJECT() && atomId)
|
||||||
|
templateViews.push({
|
||||||
|
target: { kind: "atom", atomId },
|
||||||
|
interface: state.types.interface(clause.interfaceType()!),
|
||||||
|
});
|
||||||
|
else
|
||||||
|
loweringIssue(state, clause, "invalid-query-view", "Expected declared object parameter or atom view");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (clause.OBJECT())
|
||||||
|
loweringIssue(state, clause, "invalid-query-view", "Object parameter views require a query template");
|
||||||
|
const atomId = requireSymbol(state, state.atoms, name, clause, "atom");
|
||||||
|
if (atomId) declaration.views.push({ atomId, interfaceRevisionId: close(clause.interfaceType()!) });
|
||||||
|
} else if (clause.MAX()) {
|
||||||
|
const name = identifier(clause.identifier(0));
|
||||||
|
const key = budgetNames[name],
|
||||||
|
n = Number(clause.INTEGER()!.getText());
|
||||||
|
if (!key || assigned.has(name) || !Number.isSafeInteger(n) || n < 1 || n > defaultQueryBudgets[key])
|
||||||
|
loweringIssue(
|
||||||
|
state,
|
||||||
|
clause,
|
||||||
|
"invalid-query-budget",
|
||||||
|
`Invalid, duplicate, or excessive query budget ${name}`,
|
||||||
|
);
|
||||||
|
else {
|
||||||
|
declaration.budgets[key] = n;
|
||||||
|
assigned.add(name);
|
||||||
|
}
|
||||||
|
} else if (clause.ALLOW()) {
|
||||||
|
if (parameters.length) {
|
||||||
|
const use = identifier(clause.identifier(1));
|
||||||
|
const reason = stringValue(clause.stringLiteral()!);
|
||||||
|
if (!["select", "predicate", "order"].includes(use) || !reason.trim())
|
||||||
|
loweringIssue(
|
||||||
|
state,
|
||||||
|
clause,
|
||||||
|
"invalid-query-allowance",
|
||||||
|
"Expected select/predicate/order and a nonempty reason",
|
||||||
|
);
|
||||||
|
templateAllowances.push({
|
||||||
|
interface: state.types.interface(clause.interfaceType()!),
|
||||||
|
memberName: identifier(clause.identifier(0)),
|
||||||
|
uses: [use as QueryUse],
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const interfaceRevisionId = close(clause.interfaceType()!);
|
||||||
|
const contract = [...state.types.interfaces.values(), ...state.types.applications.values()].find(
|
||||||
|
(entry) => entry.revisionId === interfaceRevisionId,
|
||||||
);
|
);
|
||||||
else {
|
const member = contract?.members.find((entry) => entry.displayName === identifier(clause.identifier(0)));
|
||||||
declaration.budgets[key] = n;
|
const use = identifier(clause.identifier(1));
|
||||||
assigned.add(name);
|
const reason = stringValue(clause.stringLiteral()!);
|
||||||
|
if (!member || !["select", "predicate", "order"].includes(use) || !reason.trim())
|
||||||
|
loweringIssue(
|
||||||
|
state,
|
||||||
|
clause,
|
||||||
|
"invalid-query-allowance",
|
||||||
|
"Query allowances name an exact member, use (select/predicate/order), and nonempty reason",
|
||||||
|
);
|
||||||
|
else
|
||||||
|
declaration.allowances.push({
|
||||||
|
interfaceRevisionId,
|
||||||
|
memberId: member.id,
|
||||||
|
uses: [use as QueryUse],
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
} else if (clause.WATCH()) declaration.watch = true;
|
||||||
|
else if (clause.POLL()) {
|
||||||
|
const intervalMs = Number(clause.INTEGER()!.getText()),
|
||||||
|
reason = stringValue(clause.stringLiteral()!);
|
||||||
|
if (declaration.polling || !Number.isSafeInteger(intervalMs) || intervalMs < 1000 || !reason.trim())
|
||||||
|
loweringIssue(
|
||||||
|
state,
|
||||||
|
clause,
|
||||||
|
"invalid-query-polling",
|
||||||
|
"Polling needs one interval of at least 1000ms and a reason",
|
||||||
|
);
|
||||||
|
else declaration.polling = { intervalMs, reason };
|
||||||
}
|
}
|
||||||
} else if (clause.ALLOW()) {
|
|
||||||
const interfaceRevisionId = close(clause.interfaceType()!);
|
|
||||||
const contract = [...state.types.interfaces.values(), ...state.types.applications.values()].find(
|
|
||||||
(entry) => entry.revisionId === interfaceRevisionId,
|
|
||||||
);
|
|
||||||
const member = contract?.members.find((entry) => entry.displayName === identifier(clause.identifier(0)));
|
|
||||||
const use = identifier(clause.identifier(1));
|
|
||||||
const reason = stringValue(clause.stringLiteral()!);
|
|
||||||
if (!member || !["select", "predicate", "order"].includes(use) || !reason.trim())
|
|
||||||
loweringIssue(
|
|
||||||
state,
|
|
||||||
clause,
|
|
||||||
"invalid-query-allowance",
|
|
||||||
"Query allowances name an exact member, use (select/predicate/order), and nonempty reason",
|
|
||||||
);
|
|
||||||
else
|
|
||||||
declaration.allowances.push({ interfaceRevisionId, memberId: member.id, uses: [use as QueryUse], reason });
|
|
||||||
} else if (clause.WATCH()) declaration.watch = true;
|
|
||||||
else if (clause.POLL()) {
|
|
||||||
const intervalMs = Number(clause.INTEGER()!.getText()),
|
|
||||||
reason = stringValue(clause.stringLiteral()!);
|
|
||||||
if (declaration.polling || !Number.isSafeInteger(intervalMs) || intervalMs < 1000 || !reason.trim())
|
|
||||||
loweringIssue(
|
|
||||||
state,
|
|
||||||
clause,
|
|
||||||
"invalid-query-polling",
|
|
||||||
"Polling needs one interval of at least 1000ms and a reason",
|
|
||||||
);
|
|
||||||
else declaration.polling = { intervalMs, reason };
|
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
[...queries, ...queryTemplates.map((entry) => entry.declaration)].some(
|
||||||
|
(entry) => entry.id === declaration.id || entry.displayName === declaration.displayName,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
loweringIssue(state, query, "duplicate-query", "Duplicate query name or ID");
|
||||||
|
if (parameters.length) {
|
||||||
|
const { root, views: _views, allowances: _allowances, ...common } = declaration;
|
||||||
|
queryTemplates.push({
|
||||||
|
declaration: common,
|
||||||
|
parameters,
|
||||||
|
root: expressions.get(root)!,
|
||||||
|
views: templateViews,
|
||||||
|
allowances: templateAllowances,
|
||||||
|
});
|
||||||
|
} else queries.push(declaration);
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
state.types.parameters = previousParameters;
|
||||||
}
|
}
|
||||||
if (queries.some((entry) => entry.id === declaration.id || entry.displayName === declaration.displayName))
|
|
||||||
loweringIssue(state, query, "duplicate-query", "Duplicate query name or ID");
|
|
||||||
queries.push(declaration);
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
const operation = exportContext.packageOperationExport();
|
const operation = exportContext.packageOperationExport();
|
||||||
const generic = operation ?? exportContext.packageFunctionExport();
|
const generic = operation ?? exportContext.packageFunctionExport();
|
||||||
@@ -1268,6 +1402,34 @@ const lowerPackage = (
|
|||||||
state.types.self = previousSelf;
|
state.types.self = previousSelf;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
for (const entry of context.packageExport()) {
|
||||||
|
const specialized = entry.packageQuerySpecialization();
|
||||||
|
if (!specialized) continue;
|
||||||
|
const templateName = identifier(specialized.identifier(1));
|
||||||
|
const template = queryTemplates.find((entry) => entry.declaration.displayName === templateName);
|
||||||
|
if (!template) {
|
||||||
|
loweringIssue(state, specialized, "unknown-query-template", `Unknown local query template ${templateName}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const substitution = new TypeSubstitution(state.types.environment());
|
||||||
|
const arguments_ = state.types
|
||||||
|
.arguments(specialized.typeArguments())
|
||||||
|
.map((argument) => substitution.argument(argument));
|
||||||
|
const declaration = specializeQueryTemplate(
|
||||||
|
template,
|
||||||
|
arguments_,
|
||||||
|
state.types.environment(),
|
||||||
|
() => [...state.types.definitions.values()],
|
||||||
|
{ id: stringValue(specialized.stringLiteral()), displayName: identifier(specialized.identifier(0)) },
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
[...queries, ...queryTemplates.map((entry) => entry.declaration)].some(
|
||||||
|
(entry) => entry.id === declaration.id || entry.displayName === declaration.displayName,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
loweringIssue(state, specialized, "duplicate-query", "Duplicate query name or ID");
|
||||||
|
queries.push(declaration);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
packageId: capabilityId.package(stringValue(context.stringLiteral(0))),
|
packageId: capabilityId.package(stringValue(context.stringLiteral(0))),
|
||||||
revisionId: state.packages.get(alias)!.revisionId,
|
revisionId: state.packages.get(alias)!.revisionId,
|
||||||
@@ -1276,6 +1438,10 @@ const lowerPackage = (
|
|||||||
source,
|
source,
|
||||||
exports,
|
exports,
|
||||||
...(queries.length ? { queries } : {}),
|
...(queries.length ? { queries } : {}),
|
||||||
|
...(queryTemplates.length ? { queryTemplates } : {}),
|
||||||
|
...(queries.some((query) => query.argumentRequirements?.length)
|
||||||
|
? { argumentRequirements: queries.flatMap((query) => query.argumentRequirements ?? []) }
|
||||||
|
: {}),
|
||||||
...(genericExports.length ? { genericExports } : {}),
|
...(genericExports.length ? { genericExports } : {}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1405,6 +1571,13 @@ const lowerBoundDependencies = (
|
|||||||
);
|
);
|
||||||
return projectionId ? { edgeTypeId: attachment.attachment.id, projectionId } : undefined;
|
return projectionId ? { edgeTypeId: attachment.attachment.id, projectionId } : undefined;
|
||||||
};
|
};
|
||||||
|
if (entry.QUERY()) {
|
||||||
|
const query = lowerQueryReference(state, identifier(entry.identifier(1)), identifier(entry.identifier(2)), entry);
|
||||||
|
const via = entry.VIA() ? traversal(identifier(entry.identifier(3)), identifier(entry.identifier(4))) : undefined;
|
||||||
|
return query && (!entry.VIA() || via)
|
||||||
|
? [{ portId, binding: { kind: "query", ...query, ...(via ? { via } : {}) } }]
|
||||||
|
: [];
|
||||||
|
}
|
||||||
if (entry.STATE()) {
|
if (entry.STATE()) {
|
||||||
const attachmentName = identifier(entry.identifier(1));
|
const attachmentName = identifier(entry.identifier(1));
|
||||||
const attachment = requireSymbol(state, state.attachments, attachmentName, entry, "attachment");
|
const attachment = requireSymbol(state, state.attachments, attachmentName, entry, "attachment");
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ type Change = { file: string; before: string | null; after: string; mode: number
|
|||||||
type Journal = { schemaVersion: 1; id: string; root: string; phase: "prepared" | "complete"; changes: Change[] };
|
type Journal = { schemaVersion: 1; id: string; root: string; phase: "prepared" | "complete"; changes: Change[] };
|
||||||
const safeFile = (file: string) => {
|
const safeFile = (file: string) => {
|
||||||
if (
|
if (
|
||||||
!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*(?:\.gitignore|\.yarnrc.yml|[A-Za-z0-9_-][A-Za-z0-9_.-]*\.(?:qx|lock|ts|tsx|css|mjs|json|nix|txtpb|md))$/.test(
|
!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*(?:\.gitignore|\.yarnrc.yml|[A-Za-z0-9_-][A-Za-z0-9_.-]*\.(?:qx|graphql|lock|ts|tsx|css|mjs|json|nix|txtpb|md))$/.test(
|
||||||
file,
|
file,
|
||||||
) ||
|
) ||
|
||||||
file.split("/").some((part) => [".git", ".jj", ".quixos", "node_modules"].includes(part))
|
file.split("/").some((part) => [".git", ".jj", ".quixos", "node_modules"].includes(part))
|
||||||
|
|||||||
@@ -132,6 +132,21 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
const dependency = (parent: GraphNode, binding: DependencyBinding, atomId: string, reviews?: Set<string>) => {
|
const dependency = (parent: GraphNode, binding: DependencyBinding, atomId: string, reviews?: Set<string>) => {
|
||||||
|
if (binding.kind === "query") {
|
||||||
|
const pkg = workspace.packageImports.find((pkg) => pkg.revisionId === binding.packageRevisionId);
|
||||||
|
const query = pkg?.checkedQueries?.find((query) => query.declaration.id === binding.queryId);
|
||||||
|
if (!query) throw new Error(`Missing checked query contract ${binding.packageRevisionId}:${binding.queryId}`);
|
||||||
|
const key = `query:${binding.packageRevisionId}:${binding.queryId}`;
|
||||||
|
const queryNode = nodes.get(key) ?? node(key, query);
|
||||||
|
parent.dependencies.add(key);
|
||||||
|
for (const effect of query.effects) {
|
||||||
|
queryNode.dependencies.add(`interface:${effect.interfaceRevisionId}`);
|
||||||
|
for (const conformance of workspace.conformances.filter(
|
||||||
|
(entry) => entry.interfaceRevisionId === effect.interfaceRevisionId,
|
||||||
|
))
|
||||||
|
queryNode.dependencies.add(conformanceKey(conformance.atomId, conformance.interfaceRevisionId));
|
||||||
|
}
|
||||||
|
}
|
||||||
if (binding.kind === "state") parent.dependencies.add(`attachment:${binding.slotId}`);
|
if (binding.kind === "state") parent.dependencies.add(`attachment:${binding.slotId}`);
|
||||||
if (binding.kind === "edge") parent.dependencies.add(`attachment:${binding.edgeTypeId}`);
|
if (binding.kind === "edge") parent.dependencies.add(`attachment:${binding.edgeTypeId}`);
|
||||||
if (binding.kind === "constructor") {
|
if (binding.kind === "constructor") {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
|
|
||||||
export type GenericDependencyPort = Omit<DependencyPort, "requirement"> & {
|
export type GenericDependencyPort = Omit<DependencyPort, "requirement"> & {
|
||||||
requirement:
|
requirement:
|
||||||
|
| Extract<DependencyPort["requirement"], { kind: "query" }>
|
||||||
| {
|
| {
|
||||||
kind: "state";
|
kind: "state";
|
||||||
valueType: ValueTypeExpression;
|
valueType: ValueTypeExpression;
|
||||||
@@ -79,6 +80,8 @@ export const specializePackageExport = (
|
|||||||
const ports: DependencyPort[] = definition.dependencyPorts.map((port) => {
|
const ports: DependencyPort[] = definition.dependencyPorts.map((port) => {
|
||||||
const requirement = port.requirement;
|
const requirement = port.requirement;
|
||||||
switch (requirement.kind) {
|
switch (requirement.kind) {
|
||||||
|
case "query":
|
||||||
|
return { ...port, requirement };
|
||||||
case "state":
|
case "state":
|
||||||
return { ...port, requirement: { ...requirement, valueType: substitution.value(requirement.valueType) } };
|
return { ...port, requirement: { ...requirement, valueType: substitution.value(requirement.valueType) } };
|
||||||
case "edge":
|
case "edge":
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export const instantiateInterface = (
|
|||||||
target: substitution.object(member.target, member.displayName),
|
target: substitution.object(member.target, member.displayName),
|
||||||
cardinality: member.cardinality,
|
cardinality: member.cardinality,
|
||||||
ordered: member.ordered,
|
ordered: member.ordered,
|
||||||
|
...(member.keyType ? { keyType: member.keyType } : {}),
|
||||||
};
|
};
|
||||||
case "operation":
|
case "operation":
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ export interface RelationshipInterfaceMember<
|
|||||||
target: Target;
|
target: Target;
|
||||||
cardinality: EdgeCardinality;
|
cardinality: EdgeCardinality;
|
||||||
ordered: boolean;
|
ordered: boolean;
|
||||||
|
keyType?: "string" | "boolean" | "int64";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A named callable capability that is not value or relationship sugar. */
|
/** A named callable capability that is not value or relationship sugar. */
|
||||||
@@ -222,6 +223,7 @@ export type PackageReceiverRequirement =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type DependencyPortRequirement =
|
export type DependencyPortRequirement =
|
||||||
|
| { kind: "query"; packageRevisionId: PackageRevisionId; queryId: string }
|
||||||
| {
|
| {
|
||||||
kind: "state";
|
kind: "state";
|
||||||
valueType: ValueType;
|
valueType: ValueType;
|
||||||
@@ -276,6 +278,7 @@ export type PackageExport = PackageOperationExport | PackageFunctionExport | Pac
|
|||||||
export interface PackageRevision {
|
export interface PackageRevision {
|
||||||
queries?: import("../query/types.js").QueryDeclaration[];
|
queries?: import("../query/types.js").QueryDeclaration[];
|
||||||
checkedQueries?: import("../query/types.js").CheckedQuery[];
|
checkedQueries?: import("../query/types.js").CheckedQuery[];
|
||||||
|
queryTemplates?: import("../query/types.js").QueryTemplate[];
|
||||||
genericExports?: import("./generic-packages.js").GenericPackageExport[];
|
genericExports?: import("./generic-packages.js").GenericPackageExport[];
|
||||||
argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[];
|
argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[];
|
||||||
migrationCatalog?: import("./migrations.js").MigrationCatalog;
|
migrationCatalog?: import("./migrations.js").MigrationCatalog;
|
||||||
@@ -289,6 +292,7 @@ export interface PackageRevision {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type DependencyBinding =
|
export type DependencyBinding =
|
||||||
|
| { kind: "query"; packageRevisionId: PackageRevisionId; queryId: string; via?: EdgeTraversal }
|
||||||
| { kind: "state"; slotId: SlotId; via?: EdgeTraversal }
|
| { kind: "state"; slotId: SlotId; via?: EdgeTraversal }
|
||||||
| {
|
| {
|
||||||
kind: "edge";
|
kind: "edge";
|
||||||
|
|||||||
@@ -504,6 +504,18 @@ const collectIdentityIndexes = (
|
|||||||
const operations = new Map<OperationId, InterfaceOperationEntry>();
|
const operations = new Map<OperationId, InterfaceOperationEntry>();
|
||||||
for (const [memberIndex, member] of revision.members.entries()) {
|
for (const [memberIndex, member] of revision.members.entries()) {
|
||||||
const memberPath = `${path}.members[${memberIndex}]`;
|
const memberPath = `${path}.members[${memberIndex}]`;
|
||||||
|
if (
|
||||||
|
member.kind === "relationship" &&
|
||||||
|
member.keyType !== undefined &&
|
||||||
|
(!["string", "boolean", "int64"].includes(member.keyType) ||
|
||||||
|
!["many", "many-unique"].includes(member.cardinality))
|
||||||
|
)
|
||||||
|
issue(
|
||||||
|
issues,
|
||||||
|
"invalid-query-contract",
|
||||||
|
memberPath,
|
||||||
|
"Keyed relationships require many cardinality and string, boolean or int64 keys",
|
||||||
|
);
|
||||||
if (member.kind !== "operation" && member.queryRead) {
|
if (member.kind !== "operation" && member.queryRead) {
|
||||||
const contract = member.queryRead.execution;
|
const contract = member.queryRead.execution;
|
||||||
const getter = member.kind === "value" ? "get" : "resolve";
|
const getter = member.kind === "value" ? "get" : "resolve";
|
||||||
@@ -984,6 +996,21 @@ const validatePackages = (
|
|||||||
for (const [portIndex, port] of entry.dependencyPorts.entries()) {
|
for (const [portIndex, port] of entry.dependencyPorts.entries()) {
|
||||||
const portPath = `${exportPath}.dependencyPorts[${portIndex}]`;
|
const portPath = `${exportPath}.dependencyPorts[${portIndex}]`;
|
||||||
switch (port.requirement.kind) {
|
switch (port.requirement.kind) {
|
||||||
|
case "query":
|
||||||
|
if (
|
||||||
|
!indexes.packages
|
||||||
|
.get(port.requirement.packageRevisionId)
|
||||||
|
?.revision.queries?.some(
|
||||||
|
(query) => port.requirement.kind === "query" && query.id === port.requirement.queryId,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
issue(
|
||||||
|
issues,
|
||||||
|
"unresolved-reference",
|
||||||
|
portPath,
|
||||||
|
`Unknown query ${port.requirement.packageRevisionId}:${port.requirement.queryId}`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
case "state":
|
case "state":
|
||||||
validateValueType(issues, port.requirement.valueType, `${portPath}.requirement.valueType`, indexes);
|
validateValueType(issues, port.requirement.valueType, `${portPath}.requirement.valueType`, indexes);
|
||||||
uniquePrimitiveList(issues, port.requirement.primitives, `${portPath}.requirement.primitives`);
|
uniquePrimitiveList(issues, port.requirement.primitives, `${portPath}.requirement.primitives`);
|
||||||
@@ -1133,6 +1160,41 @@ const validateBoundDependencies = (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
switch (requirement.kind) {
|
switch (requirement.kind) {
|
||||||
|
case "query": {
|
||||||
|
const selected = binding as Extract<DependencyBinding, { kind: "query" }>;
|
||||||
|
const query = params.indexes.packages
|
||||||
|
.get(requirement.packageRevisionId)
|
||||||
|
?.revision.queries?.find((query) => query.id === requirement.queryId);
|
||||||
|
if (
|
||||||
|
!query ||
|
||||||
|
selected.queryId !== requirement.queryId ||
|
||||||
|
selected.packageRevisionId !== requirement.packageRevisionId
|
||||||
|
) {
|
||||||
|
issue(issues, "invalid-dependency-binding", path, "Query port must bind its exact checked query export");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const traversal = selected.via
|
||||||
|
? validateTraversal(
|
||||||
|
issues,
|
||||||
|
params.indexes,
|
||||||
|
params.atomId,
|
||||||
|
selected.via,
|
||||||
|
`${path}.binding.via`,
|
||||||
|
params.conformance,
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
const targets = selected.via
|
||||||
|
? traversal
|
||||||
|
? [...params.indexes.atoms.keys()].filter((atom) =>
|
||||||
|
atomSatisfiesConstraint(atom as AtomId, traversal.target.constraint, params.indexes.conformances),
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
: [params.atomId];
|
||||||
|
for (const atom of targets)
|
||||||
|
if (!params.indexes.conformances.has(conformanceKey(atom as AtomId, query.root)))
|
||||||
|
issue(issues, "unsatisfied-interface", path, `Query root ${atom} does not conform to ${query.root}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "state": {
|
case "state": {
|
||||||
const stateBinding = binding as Extract<DependencyBinding, { kind: "state" }>;
|
const stateBinding = binding as Extract<DependencyBinding, { kind: "state" }>;
|
||||||
const attachment = findAttachment(params.indexes, "state", stateBinding.slotId);
|
const attachment = findAttachment(params.indexes, "state", stateBinding.slotId);
|
||||||
@@ -1583,7 +1645,8 @@ const validateConformances = (
|
|||||||
operationEntry.member.target,
|
operationEntry.member.target,
|
||||||
indexes.conformances,
|
indexes.conformances,
|
||||||
) ||
|
) ||
|
||||||
operationEntry.member.cardinality !== projection.endpoint.cardinality
|
operationEntry.member.cardinality !== projection.endpoint.cardinality ||
|
||||||
|
(operationEntry.member.keyType !== undefined && operationEntry.member.keyType !== projection.endpoint.keyType)
|
||||||
) {
|
) {
|
||||||
issue(
|
issue(
|
||||||
issues,
|
issues,
|
||||||
@@ -2091,6 +2154,21 @@ export const computeCapabilityClosure = (
|
|||||||
const includeDependencies = (atomId: AtomId, dependencies: readonly BoundDependency[]) => {
|
const includeDependencies = (atomId: AtomId, dependencies: readonly BoundDependency[]) => {
|
||||||
for (const dependency of dependencies) {
|
for (const dependency of dependencies) {
|
||||||
switch (dependency.binding.kind) {
|
switch (dependency.binding.kind) {
|
||||||
|
case "query": {
|
||||||
|
packages.add(dependency.binding.packageRevisionId);
|
||||||
|
const query = plan.packages
|
||||||
|
.get(dependency.binding.packageRevisionId)
|
||||||
|
?.checkedQueries?.find(
|
||||||
|
(query) => dependency.binding.kind === "query" && query.declaration.id === dependency.binding.queryId,
|
||||||
|
);
|
||||||
|
if (dependency.binding.via) attachments.add(dependency.binding.via.edgeTypeId);
|
||||||
|
for (const effect of query?.effects ?? []) {
|
||||||
|
for (const conformance of sourceConformances.values())
|
||||||
|
if (conformance.interfaceRevisionId === effect.interfaceRevisionId)
|
||||||
|
queued.push({ atomId: conformance.atomId, interfaceRevisionId: conformance.interfaceRevisionId });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "state":
|
case "state":
|
||||||
attachments.add(dependency.binding.slotId);
|
attachments.add(dependency.binding.slotId);
|
||||||
if (dependency.binding.via) {
|
if (dependency.binding.via) {
|
||||||
|
|||||||
+413
-2
File diff suppressed because one or more lines are too long
@@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf";
|
|||||||
* Describes the file camino/schema.proto.
|
* Describes the file camino/schema.proto.
|
||||||
*/
|
*/
|
||||||
export const file_camino_schema: GenFile = /*@__PURE__*/
|
export const file_camino_schema: GenFile = /*@__PURE__*/
|
||||||
fileDesc("ChNjYW1pbm8vc2NoZW1hLnByb3RvEgZjYW1pbm8iNwoOQXRvbURlZmluaXRpb24SDwoHYXRvbV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkiWQoPQXRvbUNvbmZvcm1hbmNlEg8KB2F0b21faWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJIsIBCg9TdGF0ZUF0dGFjaG1lbnQSDwoHc2xvdF9pZBgBIAEoCRIYChBhdHRhY2hlZF9hdG9tX2lkGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCRIXCg92YWx1ZV90eXBlX2pzb24YBCABKAkSGwoTc3RvcmFnZV9wb2xpY3lfanNvbhgFIAEoCRIaChJkZWZhdWx0X3ZhbHVlX2pzb24YBiABKAkSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYByABKAkiUAoSRW5kcG9pbnRDb25zdHJhaW50EhEKB2F0b21faWQYASABKAlIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAlIAEIGCgRraW5kIvsBCgxFZGdlRW5kcG9pbnQSFQoNcHJvamVjdGlvbl9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSLgoKY29uc3RyYWludBgDIAEoCzIaLmNhbWluby5FbmRwb2ludENvbnN0cmFpbnQSKAoLY2FyZGluYWxpdHkYBCABKA4yEy5jYW1pbm8uQ2FyZGluYWxpdHkSDwoHb3JkZXJlZBgFIAEoCBIRCglvbl9kZWxldGUYBiABKAkSFAoMcmV0YWluX290aGVyGAcgASgIEhAKCGtleV90eXBlGAggASgJEhgKEHB1YmxpY190cmF2ZXJzYWwYCSABKAgipQEKDkVkZ2VBdHRhY2htZW50EhQKDGVkZ2VfdHlwZV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSIwoFZmlyc3QYAyABKAsyFC5jYW1pbm8uRWRnZUVuZHBvaW50EiQKBnNlY29uZBgEIAEoCzIULmNhbWluby5FZGdlRW5kcG9pbnQSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYBSABKAkilQIKD1BlcnNpc3RlbmNlUGxhbhIUCgx3b3Jrc3BhY2VfaWQYASABKAkSHQoVd29ya3NwYWNlX3JldmlzaW9uX2lkGAIgASgJEiUKBWF0b21zGAMgAygLMhYuY2FtaW5vLkF0b21EZWZpbml0aW9uEi0KDGNvbmZvcm1hbmNlcxgEIAMoCzIXLmNhbWluby5BdG9tQ29uZm9ybWFuY2USJwoGc3RhdGVzGAUgAygLMhcuY2FtaW5vLlN0YXRlQXR0YWNobWVudBIlCgVlZGdlcxgGIAMoCzIWLmNhbWluby5FZGdlQXR0YWNobWVudBInCgdxdWVyaWVzGAcgAygLMhYuY2FtaW5vLkluc3RhbGxlZFF1ZXJ5Ip4BCg1RdWVyeUFyZ3VtZW50EhIKCHZhcmlhYmxlGAEgASgJSAASFgoMbGl0ZXJhbF9qc29uGAIgASgJSAASKQoEbGlzdBgDIAEoCzIZLmNhbWluby5RdWVyeUFyZ3VtZW50TGlzdEgAEi0KBm9iamVjdBgEIAEoCzIbLmNhbWluby5RdWVyeUFyZ3VtZW50T2JqZWN0SABCBwoFdmFsdWUiOgoRUXVlcnlBcmd1bWVudExpc3QSJQoGdmFsdWVzGAEgAygLMhUuY2FtaW5vLlF1ZXJ5QXJndW1lbnQilAEKE1F1ZXJ5QXJndW1lbnRPYmplY3QSNwoGZmllbGRzGAEgAygLMicuY2FtaW5vLlF1ZXJ5QXJndW1lbnRPYmplY3QuRmllbGRzRW50cnkaRAoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEiQKBXZhbHVlGAIgASgLMhUuY2FtaW5vLlF1ZXJ5QXJndW1lbnQ6AjgBIkcKDlF1ZXJ5Q29uZGl0aW9uEg8KB2luY2x1ZGUYASABKAgSJAoFdmFsdWUYAiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudCLdAgoOUXVlcnlTZWxlY3Rpb24SDAoEbmFtZRgBIAEoCRILCgNrZXkYAiABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAMgASgJEhEKCW1lbWJlcl9pZBgEIAEoCRIkChx0YXJnZXRfaW50ZXJmYWNlX3JldmlzaW9uX2lkGAUgASgJEioKCmNvbmRpdGlvbnMYBiADKAsyFi5jYW1pbm8uUXVlcnlDb25kaXRpb24SOAoJYXJndW1lbnRzGAcgAygLMiUuY2FtaW5vLlF1ZXJ5U2VsZWN0aW9uLkFyZ3VtZW50c0VudHJ5EikKCXNlbGVjdGlvbhgIIAMoCzIWLmNhbWluby5RdWVyeVNlbGVjdGlvbhpHCg5Bcmd1bWVudHNFbnRyeRILCgNrZXkYASABKAkSJAoFdmFsdWUYAiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudDoCOAEi1wIKEFF1ZXJ5UmVhZEJpbmRpbmcSDwoHYXRvbV9pZBgBIAEoCRIdChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAkSEQoJbWVtYmVyX2lkGAMgASgJEhsKE2dldHRlcl9vcGVyYXRpb25faWQYBCABKAkSDwoHc2xvdF9pZBgFIAEoCRIUCgxlZGdlX3R5cGVfaWQYBiABKAkSFQoNcHJvamVjdGlvbl9pZBgHIAEoCRILCgNycGMYCCABKAgSIAoYd2F0Y2hfc3RhcnRfb3BlcmF0aW9uX2lkGAkgASgJEh8KF3dhdGNoX3N0b3Bfb3BlcmF0aW9uX2lkGAogASgJEhIKCmZpZWxkX25hbWUYCyABKAkSFwoPdmFsdWVfdHlwZV9qc29uGAwgASgJEigKC2NhcmRpbmFsaXR5GA0gASgOMhMuY2FtaW5vLkNhcmRpbmFsaXR5IpIBCgxRdWVyeUJ1ZGdldHMSDAoEcm93cxgBIAEoDRINCgVkZXB0aBgCIAEoDRIUCgxyZXN1bHRfYnl0ZXMYAyABKA0SEgoKY2FuZGlkYXRlcxgEIAEoDRIRCglycGNfY2FsbHMYBSABKA0SEwoLY29uY3VycmVuY3kYBiABKA0SEwoLZGVhZGxpbmVfbXMYByABKA0ijQQKDkluc3RhbGxlZFF1ZXJ5EgoKAmlkGAEgASgJEhkKEWRlZmluaXRpb25fZGlnZXN0GAIgASgJEhYKDmJpbmRpbmdfZGlnZXN0GAMgASgJEiIKGnJvb3RfaW50ZXJmYWNlX3JldmlzaW9uX2lkGAQgASgJEikKCXNlbGVjdGlvbhgFIAMoCzIWLmNhbWluby5RdWVyeVNlbGVjdGlvbhIqCghiaW5kaW5ncxgGIAMoCzIYLmNhbWluby5RdWVyeVJlYWRCaW5kaW5nEiUKB2J1ZGdldHMYByABKAsyFC5jYW1pbm8uUXVlcnlCdWRnZXRzEhsKE3ZhcmlhYmxlc190eXBlX2pzb24YCCABKAkSGAoQb3V0cHV0X3R5cGVfanNvbhgJIAEoCRJHChF2YXJpYWJsZV9kZWZhdWx0cxgKIAMoCzIsLmNhbWluby5JbnN0YWxsZWRRdWVyeS5WYXJpYWJsZURlZmF1bHRzRW50cnkSDQoFd2F0Y2gYCyABKAgSGwoTcG9sbGluZ19pbnRlcnZhbF9tcxgMIAEoDRIeChZycGNfcHJlZGljYXRlX29yX29yZGVyGA0gASgIGk4KFVZhcmlhYmxlRGVmYXVsdHNFbnRyeRILCgNrZXkYASABKAkSJAoFdmFsdWUYAiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudDoCOAEqaAoLQ2FyZGluYWxpdHkSGwoXQ0FSRElOQUxJVFlfVU5TUEVDSUZJRUQQABIQCgxPUFRJT05BTF9PTkUQARIPCgtFWEFDVExZX09ORRACEggKBE1BTlkQAxIPCgtNQU5ZX1VOSVFVRRAEYgZwcm90bzM");
|
fileDesc("ChNjYW1pbm8vc2NoZW1hLnByb3RvEgZjYW1pbm8iNwoOQXRvbURlZmluaXRpb24SDwoHYXRvbV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkiWQoPQXRvbUNvbmZvcm1hbmNlEg8KB2F0b21faWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJIsIBCg9TdGF0ZUF0dGFjaG1lbnQSDwoHc2xvdF9pZBgBIAEoCRIYChBhdHRhY2hlZF9hdG9tX2lkGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCRIXCg92YWx1ZV90eXBlX2pzb24YBCABKAkSGwoTc3RvcmFnZV9wb2xpY3lfanNvbhgFIAEoCRIaChJkZWZhdWx0X3ZhbHVlX2pzb24YBiABKAkSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYByABKAkiUAoSRW5kcG9pbnRDb25zdHJhaW50EhEKB2F0b21faWQYASABKAlIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAlIAEIGCgRraW5kIvsBCgxFZGdlRW5kcG9pbnQSFQoNcHJvamVjdGlvbl9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSLgoKY29uc3RyYWludBgDIAEoCzIaLmNhbWluby5FbmRwb2ludENvbnN0cmFpbnQSKAoLY2FyZGluYWxpdHkYBCABKA4yEy5jYW1pbm8uQ2FyZGluYWxpdHkSDwoHb3JkZXJlZBgFIAEoCBIRCglvbl9kZWxldGUYBiABKAkSFAoMcmV0YWluX290aGVyGAcgASgIEhAKCGtleV90eXBlGAggASgJEhgKEHB1YmxpY190cmF2ZXJzYWwYCSABKAgipQEKDkVkZ2VBdHRhY2htZW50EhQKDGVkZ2VfdHlwZV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSIwoFZmlyc3QYAyABKAsyFC5jYW1pbm8uRWRnZUVuZHBvaW50EiQKBnNlY29uZBgEIAEoCzIULmNhbWluby5FZGdlRW5kcG9pbnQSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYBSABKAkilQIKD1BlcnNpc3RlbmNlUGxhbhIUCgx3b3Jrc3BhY2VfaWQYASABKAkSHQoVd29ya3NwYWNlX3JldmlzaW9uX2lkGAIgASgJEiUKBWF0b21zGAMgAygLMhYuY2FtaW5vLkF0b21EZWZpbml0aW9uEi0KDGNvbmZvcm1hbmNlcxgEIAMoCzIXLmNhbWluby5BdG9tQ29uZm9ybWFuY2USJwoGc3RhdGVzGAUgAygLMhcuY2FtaW5vLlN0YXRlQXR0YWNobWVudBIlCgVlZGdlcxgGIAMoCzIWLmNhbWluby5FZGdlQXR0YWNobWVudBInCgdxdWVyaWVzGAcgAygLMhYuY2FtaW5vLkluc3RhbGxlZFF1ZXJ5Ip4BCg1RdWVyeUFyZ3VtZW50EhIKCHZhcmlhYmxlGAEgASgJSAASFgoMbGl0ZXJhbF9qc29uGAIgASgJSAASKQoEbGlzdBgDIAEoCzIZLmNhbWluby5RdWVyeUFyZ3VtZW50TGlzdEgAEi0KBm9iamVjdBgEIAEoCzIbLmNhbWluby5RdWVyeUFyZ3VtZW50T2JqZWN0SABCBwoFdmFsdWUiOgoRUXVlcnlBcmd1bWVudExpc3QSJQoGdmFsdWVzGAEgAygLMhUuY2FtaW5vLlF1ZXJ5QXJndW1lbnQilAEKE1F1ZXJ5QXJndW1lbnRPYmplY3QSNwoGZmllbGRzGAEgAygLMicuY2FtaW5vLlF1ZXJ5QXJndW1lbnRPYmplY3QuRmllbGRzRW50cnkaRAoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEiQKBXZhbHVlGAIgASgLMhUuY2FtaW5vLlF1ZXJ5QXJndW1lbnQ6AjgBIkcKDlF1ZXJ5Q29uZGl0aW9uEg8KB2luY2x1ZGUYASABKAgSJAoFdmFsdWUYAiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudCLdAgoOUXVlcnlTZWxlY3Rpb24SDAoEbmFtZRgBIAEoCRILCgNrZXkYAiABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAMgASgJEhEKCW1lbWJlcl9pZBgEIAEoCRIkChx0YXJnZXRfaW50ZXJmYWNlX3JldmlzaW9uX2lkGAUgASgJEioKCmNvbmRpdGlvbnMYBiADKAsyFi5jYW1pbm8uUXVlcnlDb25kaXRpb24SOAoJYXJndW1lbnRzGAcgAygLMiUuY2FtaW5vLlF1ZXJ5U2VsZWN0aW9uLkFyZ3VtZW50c0VudHJ5EikKCXNlbGVjdGlvbhgIIAMoCzIWLmNhbWluby5RdWVyeVNlbGVjdGlvbhpHCg5Bcmd1bWVudHNFbnRyeRILCgNrZXkYASABKAkSJAoFdmFsdWUYAiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudDoCOAEi6QIKEFF1ZXJ5UmVhZEJpbmRpbmcSDwoHYXRvbV9pZBgBIAEoCRIdChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAkSEQoJbWVtYmVyX2lkGAMgASgJEhsKE2dldHRlcl9vcGVyYXRpb25faWQYBCABKAkSDwoHc2xvdF9pZBgFIAEoCRIUCgxlZGdlX3R5cGVfaWQYBiABKAkSFQoNcHJvamVjdGlvbl9pZBgHIAEoCRILCgNycGMYCCABKAgSIAoYd2F0Y2hfc3RhcnRfb3BlcmF0aW9uX2lkGAkgASgJEh8KF3dhdGNoX3N0b3Bfb3BlcmF0aW9uX2lkGAogASgJEhIKCmZpZWxkX25hbWUYCyABKAkSFwoPdmFsdWVfdHlwZV9qc29uGAwgASgJEigKC2NhcmRpbmFsaXR5GA0gASgOMhMuY2FtaW5vLkNhcmRpbmFsaXR5EhAKCGtleV90eXBlGA4gASgJIpIBCgxRdWVyeUJ1ZGdldHMSDAoEcm93cxgBIAEoDRINCgVkZXB0aBgCIAEoDRIUCgxyZXN1bHRfYnl0ZXMYAyABKA0SEgoKY2FuZGlkYXRlcxgEIAEoDRIRCglycGNfY2FsbHMYBSABKA0SEwoLY29uY3VycmVuY3kYBiABKA0SEwoLZGVhZGxpbmVfbXMYByABKA0ijQQKDkluc3RhbGxlZFF1ZXJ5EgoKAmlkGAEgASgJEhkKEWRlZmluaXRpb25fZGlnZXN0GAIgASgJEhYKDmJpbmRpbmdfZGlnZXN0GAMgASgJEiIKGnJvb3RfaW50ZXJmYWNlX3JldmlzaW9uX2lkGAQgASgJEikKCXNlbGVjdGlvbhgFIAMoCzIWLmNhbWluby5RdWVyeVNlbGVjdGlvbhIqCghiaW5kaW5ncxgGIAMoCzIYLmNhbWluby5RdWVyeVJlYWRCaW5kaW5nEiUKB2J1ZGdldHMYByABKAsyFC5jYW1pbm8uUXVlcnlCdWRnZXRzEhsKE3ZhcmlhYmxlc190eXBlX2pzb24YCCABKAkSGAoQb3V0cHV0X3R5cGVfanNvbhgJIAEoCRJHChF2YXJpYWJsZV9kZWZhdWx0cxgKIAMoCzIsLmNhbWluby5JbnN0YWxsZWRRdWVyeS5WYXJpYWJsZURlZmF1bHRzRW50cnkSDQoFd2F0Y2gYCyABKAgSGwoTcG9sbGluZ19pbnRlcnZhbF9tcxgMIAEoDRIeChZycGNfcHJlZGljYXRlX29yX29yZGVyGA0gASgIGk4KFVZhcmlhYmxlRGVmYXVsdHNFbnRyeRILCgNrZXkYASABKAkSJAoFdmFsdWUYAiABKAsyFS5jYW1pbm8uUXVlcnlBcmd1bWVudDoCOAEqaAoLQ2FyZGluYWxpdHkSGwoXQ0FSRElOQUxJVFlfVU5TUEVDSUZJRUQQABIQCgxPUFRJT05BTF9PTkUQARIPCgtFWEFDVExZX09ORRACEggKBE1BTlkQAxIPCgtNQU5ZX1VOSVFVRRAEYgZwcm90bzM");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message camino.AtomDefinition
|
* @generated from message camino.AtomDefinition
|
||||||
@@ -507,6 +507,11 @@ export type QueryReadBinding = Message<"camino.QueryReadBinding"> & {
|
|||||||
* @generated from field: camino.Cardinality cardinality = 13;
|
* @generated from field: camino.Cardinality cardinality = 13;
|
||||||
*/
|
*/
|
||||||
cardinality: Cardinality;
|
cardinality: Cardinality;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: string key_type = 14;
|
||||||
|
*/
|
||||||
|
keyType: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+162
-30
File diff suppressed because one or more lines are too long
@@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf";
|
|||||||
* Describes the file quixos/refs.proto.
|
* Describes the file quixos/refs.proto.
|
||||||
*/
|
*/
|
||||||
export const file_quixos_refs: GenFile = /*@__PURE__*/
|
export const file_quixos_refs: GenFile = /*@__PURE__*/
|
||||||
fileDesc("ChFxdWl4b3MvcmVmcy5wcm90bxIGcXVpeG9zInUKDUNhcGFiaWxpdHlSZWYSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhQKDG9wZXJhdGlvbl9pZBgCIAEoCRIvCgtjb25mb3JtYW5jZRgDIAEoCzIaLnF1aXhvcy5Db25mb3JtYW5jZVdpdG5lc3MilgEKEkNvbmZvcm1hbmNlV2l0bmVzcxIRCglvYmplY3RfaWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJEh0KFXdvcmtzcGFjZV9yZXZpc2lvbl9pZBgEIAEoCRIXCg93b3Jrc3BhY2VfZXBvY2gYBSABKAkiQgoQUGFja2FnZUV4cG9ydFJlZhIbChNwYWNrYWdlX3JldmlzaW9uX2lkGAEgASgJEhEKCWV4cG9ydF9pZBgCIAEoCSLEAQoSSW5qZWN0ZWREZXBlbmRlbmN5Eg8KB3BvcnRfaWQYASABKAkSFwoNc3RhdGVfc2xvdF9pZBgCIAEoCUgAEiYKBGVkZ2UYAyABKAsyFi5xdWl4b3MuRWRnZURlcGVuZGVuY3lIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYBCABKAlIABIdChNjb25zdHJ1Y3Rvcl9hdG9tX2lkGAUgASgJSAASEQoJb2JqZWN0X2lkGAYgASgJQgkKB2JpbmRpbmciPQoORWRnZURlcGVuZGVuY3kSFAoMZWRnZV90eXBlX2lkGAEgASgJEhUKDXByb2plY3Rpb25faWQYAiABKAliBnByb3RvMw");
|
fileDesc("ChFxdWl4b3MvcmVmcy5wcm90bxIGcXVpeG9zInUKDUNhcGFiaWxpdHlSZWYSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhQKDG9wZXJhdGlvbl9pZBgCIAEoCRIvCgtjb25mb3JtYW5jZRgDIAEoCzIaLnF1aXhvcy5Db25mb3JtYW5jZVdpdG5lc3MilgEKEkNvbmZvcm1hbmNlV2l0bmVzcxIRCglvYmplY3RfaWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJEh0KFXdvcmtzcGFjZV9yZXZpc2lvbl9pZBgEIAEoCRIXCg93b3Jrc3BhY2VfZXBvY2gYBSABKAkiQgoQUGFja2FnZUV4cG9ydFJlZhIbChNwYWNrYWdlX3JldmlzaW9uX2lkGAEgASgJEhEKCWV4cG9ydF9pZBgCIAEoCSLYAQoSSW5qZWN0ZWREZXBlbmRlbmN5Eg8KB3BvcnRfaWQYASABKAkSFwoNc3RhdGVfc2xvdF9pZBgCIAEoCUgAEiYKBGVkZ2UYAyABKAsyFi5xdWl4b3MuRWRnZURlcGVuZGVuY3lIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYBCABKAlIABIdChNjb25zdHJ1Y3Rvcl9hdG9tX2lkGAUgASgJSAASEgoIcXVlcnlfaWQYByABKAlIABIRCglvYmplY3RfaWQYBiABKAlCCQoHYmluZGluZyI9Cg5FZGdlRGVwZW5kZW5jeRIUCgxlZGdlX3R5cGVfaWQYASABKAkSFQoNcHJvamVjdGlvbl9pZBgCIAEoCWIGcHJvdG8z");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message quixos.CapabilityRef
|
* @generated from message quixos.CapabilityRef
|
||||||
@@ -136,6 +136,12 @@ export type InjectedDependency = Message<"quixos.InjectedDependency"> & {
|
|||||||
*/
|
*/
|
||||||
value: string;
|
value: string;
|
||||||
case: "constructorAtomId";
|
case: "constructorAtomId";
|
||||||
|
} | {
|
||||||
|
/**
|
||||||
|
* @generated from field: string query_id = 7;
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
case: "queryId";
|
||||||
} | { case: undefined; value?: undefined };
|
} | { case: undefined; value?: undefined };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+68
-16
@@ -25,6 +25,7 @@ import {
|
|||||||
type DirectiveNode,
|
type DirectiveNode,
|
||||||
} from "graphql";
|
} from "graphql";
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
|
import { canonicalJson } from "../capability-model/evolution.js";
|
||||||
import { readRepositorySource } from "../capability-language/source-loader.js";
|
import { readRepositorySource } from "../capability-language/source-loader.js";
|
||||||
import {
|
import {
|
||||||
valueType,
|
valueType,
|
||||||
@@ -192,18 +193,28 @@ export async function compileQuery(
|
|||||||
if (member.kind !== "relationship" || (member.cardinality !== "many" && member.cardinality !== "many-unique"))
|
if (member.kind !== "relationship" || (member.cardinality !== "many" && member.cardinality !== "many-unique"))
|
||||||
return;
|
return;
|
||||||
const target = generated.target(member);
|
const target = generated.target(member);
|
||||||
|
const bounds = (node.arguments ?? []).filter(
|
||||||
|
(argument) => argument.name.value === "first" || argument.name.value === "all",
|
||||||
|
);
|
||||||
|
if (bounds.length !== 1) fail("QUERY_ROW_LIMIT", "Specify exactly one of first or all", node);
|
||||||
|
if (bounds[0]!.name.value === "all" && node.arguments?.some((argument) => argument.name.value === "after"))
|
||||||
|
fail("QUERY_UNSUPPORTED_FEATURE", "Bounded-all does not accept a continuation", node);
|
||||||
for (const argument of node.arguments ?? []) {
|
for (const argument of node.arguments ?? []) {
|
||||||
if (argument.name.value === "where") inputEffects(target, argument.value, "predicate");
|
if (argument.name.value === "where") inputEffects(target, argument.value, "predicate");
|
||||||
if (argument.name.value === "orderBy") inputEffects(target, argument.value, "order");
|
if (argument.name.value === "orderBy") inputEffects(target, argument.value, "order");
|
||||||
if (argument.name.value === "after") hasContinuation = true;
|
if (argument.name.value === "after") hasContinuation = true;
|
||||||
if (
|
if (
|
||||||
argument.name.value === "first" &&
|
["first", "all"].includes(argument.name.value) &&
|
||||||
argument.value.kind !== "Variable" &&
|
argument.value.kind !== "Variable" &&
|
||||||
(argument.value.kind !== "IntValue" ||
|
(argument.value.kind !== "IntValue" ||
|
||||||
Number(argument.value.value) < 1 ||
|
Number(argument.value.value) < 1 ||
|
||||||
Number(argument.value.value) > declaration.budgets.rows)
|
Number(argument.value.value) > declaration.budgets.rows)
|
||||||
)
|
)
|
||||||
fail("QUERY_ROW_LIMIT", `first must be between 1 and ${declaration.budgets.rows}`, argument);
|
fail(
|
||||||
|
"QUERY_ROW_LIMIT",
|
||||||
|
`${argument.name.value} must be between 1 and ${declaration.budgets.rows}`,
|
||||||
|
argument,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
FragmentSpread(node) {
|
FragmentSpread(node) {
|
||||||
@@ -228,26 +239,47 @@ export async function compileQuery(
|
|||||||
.map((entry) => [entry.name.value, entry]),
|
.map((entry) => [entry.name.value, entry]),
|
||||||
);
|
);
|
||||||
let expandedFields = 0;
|
let expandedFields = 0;
|
||||||
const output = (type: GraphQLType, selections?: SelectionSetNode, depth = 0): ValueType => {
|
// GraphQL merges repeated response keys. In particular, fragments may each
|
||||||
if (depth > declaration.budgets.depth * 4 + 4) fail("QUERY_DEPTH_LIMIT", "Expanded query exceeds its depth budget");
|
// contribute different children of one relationship; last-write-wins would
|
||||||
if (isNonNullType(type)) return required(type.ofType, selections, depth);
|
// silently remove fields from both the generated type and the wire codec.
|
||||||
return valueType.optional(required(type, selections, depth));
|
const mergeOutput = (left: ValueType, right: ValueType): ValueType => {
|
||||||
|
const a = left.kind === "optional" ? left.value : left;
|
||||||
|
const b = right.kind === "optional" ? right.value : right;
|
||||||
|
let result = a;
|
||||||
|
if (a.kind === "record" && b.kind === "record") {
|
||||||
|
const fields = { ...a.fields };
|
||||||
|
for (const [key, type] of Object.entries(b.fields))
|
||||||
|
fields[key] = fields[key] ? mergeOutput(fields[key], type) : type;
|
||||||
|
result = { kind: "record", fields };
|
||||||
|
} else if (a.kind === "list" && b.kind === "list") result = valueType.list(mergeOutput(a.value, b.value));
|
||||||
|
return left.kind === "optional" && right.kind === "optional" ? valueType.optional(result) : result;
|
||||||
};
|
};
|
||||||
const required = (type: GraphQLType, selections?: SelectionSetNode, depth = 0): ValueType => {
|
const output = (type: GraphQLType, selections?: SelectionSetNode, depth = 0, conditional = false): ValueType => {
|
||||||
if (isListType(type)) return valueType.list(output(type.ofType, selections, depth));
|
if (depth > declaration.budgets.depth * 4 + 4) fail("QUERY_DEPTH_LIMIT", "Expanded query exceeds its depth budget");
|
||||||
|
if (isNonNullType(type)) return required(type.ofType, selections, depth, conditional);
|
||||||
|
return valueType.optional(required(type, selections, depth, conditional));
|
||||||
|
};
|
||||||
|
const required = (type: GraphQLType, selections?: SelectionSetNode, depth = 0, conditional = false): ValueType => {
|
||||||
|
if (isListType(type)) return valueType.list(output(type.ofType, selections, depth, conditional));
|
||||||
if (isObjectType(type)) {
|
if (isObjectType(type)) {
|
||||||
const fields: Record<string, ValueType> = {};
|
const fields: Record<string, ValueType> = {};
|
||||||
const add = (set: SelectionSetNode) => {
|
const add = (set: SelectionSetNode, conditional = false) => {
|
||||||
for (const selection of set.selections) {
|
for (const selection of set.selections) {
|
||||||
if (++expandedFields > 10000) fail("QUERY_WORK_LIMIT", "Expanded query exceeds 10000 fields", selection);
|
if (++expandedFields > 10000) fail("QUERY_WORK_LIMIT", "Expanded query exceeds 10000 fields", selection);
|
||||||
if (selection.kind === "FragmentSpread") {
|
if (selection.kind === "FragmentSpread") {
|
||||||
add(fragments.get(selection.name.value)!.selectionSet);
|
add(fragments.get(selection.name.value)!.selectionSet, conditional || !!selection.directives?.length);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (selection.kind !== "Field") continue;
|
if (selection.kind !== "Field") continue;
|
||||||
const key = selection.alias?.value ?? selection.name.value;
|
const key = selection.alias?.value ?? selection.name.value;
|
||||||
const field = type.getFields()[selection.name.value]!;
|
const field = type.getFields()[selection.name.value]!;
|
||||||
fields[key] = output(field.type, selection.selectionSet, depth + 1);
|
const previous = fields[key];
|
||||||
|
fields[key] = output(
|
||||||
|
field.type,
|
||||||
|
selection.selectionSet,
|
||||||
|
depth + 1,
|
||||||
|
conditional || !!selection.directives?.length,
|
||||||
|
);
|
||||||
if (selection.name.value === "_qx") {
|
if (selection.name.value === "_qx") {
|
||||||
const contract = generated.byName.get(type.name)!;
|
const contract = generated.byName.get(type.name)!;
|
||||||
const metadataFields: Record<string, ValueType> = {};
|
const metadataFields: Record<string, ValueType> = {};
|
||||||
@@ -258,11 +290,12 @@ export async function compileQuery(
|
|||||||
}
|
}
|
||||||
fields[key] = { kind: "record", fields: metadataFields };
|
fields[key] = { kind: "record", fields: metadataFields };
|
||||||
}
|
}
|
||||||
if (selection.directives?.length && fields[key]!.kind !== "optional")
|
if ((conditional || selection.directives?.length) && fields[key]!.kind !== "optional")
|
||||||
fields[key] = valueType.optional(fields[key]!);
|
fields[key] = valueType.optional(fields[key]!);
|
||||||
|
if (previous) fields[key] = mergeOutput(previous, fields[key]!);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (selections) add(selections);
|
if (selections) add(selections, conditional);
|
||||||
return { kind: "record", fields };
|
return { kind: "record", fields };
|
||||||
}
|
}
|
||||||
return shapeScalar(getNamedType(type)!.name);
|
return shapeScalar(getNamedType(type)!.name);
|
||||||
@@ -354,7 +387,16 @@ export async function compileQuery(
|
|||||||
const normalized = print(document),
|
const normalized = print(document),
|
||||||
schema = printSchema(generated.schema);
|
schema = printSchema(generated.schema);
|
||||||
const definitionDigest = createHash("sha256")
|
const definitionDigest = createHash("sha256")
|
||||||
.update(JSON.stringify({ semantics: 1, declaration, normalized, interfaces }))
|
.update(
|
||||||
|
canonicalJson({
|
||||||
|
semantics: 1,
|
||||||
|
declaration,
|
||||||
|
normalized,
|
||||||
|
interfaces: [...generated.byName.values()].sort((a, b) =>
|
||||||
|
a.revisionId < b.revisionId ? -1 : a.revisionId > b.revisionId ? 1 : 0,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
)
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
return {
|
return {
|
||||||
declaration,
|
declaration,
|
||||||
@@ -374,8 +416,18 @@ export async function compileQuery(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const compileRepositoryQuery = (
|
export const compileRepositoryQuery = async (
|
||||||
root: string,
|
root: string,
|
||||||
declaration: QueryDeclaration,
|
declaration: QueryDeclaration,
|
||||||
interfaces: readonly InterfaceRevision[],
|
interfaces: readonly InterfaceRevision[],
|
||||||
) => compileQuery(declaration, interfaces, (name) => readRepositorySource(root, name));
|
) => {
|
||||||
|
const started = performance.now();
|
||||||
|
try {
|
||||||
|
return await compileQuery(declaration, interfaces, (name) => readRepositorySource(root, name));
|
||||||
|
} finally {
|
||||||
|
// Timing belongs in check logs, never in immutable artifact identities.
|
||||||
|
console.error(
|
||||||
|
`[query-check] ${JSON.stringify(declaration.displayName)}: ${(performance.now() - started).toFixed(1)}ms`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
+13
-1
@@ -9,6 +9,7 @@ import type {
|
|||||||
} from "../capability-model/types.js";
|
} from "../capability-model/types.js";
|
||||||
import { QueryCompileError, type CheckedQuery } from "./types.js";
|
import { QueryCompileError, type CheckedQuery } from "./types.js";
|
||||||
import { queryRuntimePlan } from "./proto.js";
|
import { queryRuntimePlan } from "./proto.js";
|
||||||
|
import { canonicalJson } from "../capability-model/evolution.js";
|
||||||
|
|
||||||
export interface LinkedQueryField {
|
export interface LinkedQueryField {
|
||||||
atomId: AtomId;
|
atomId: AtomId;
|
||||||
@@ -128,7 +129,13 @@ export function linkQueries(workspace: WorkspaceRevision): LinkedQuery[] {
|
|||||||
}
|
}
|
||||||
const storage = attachments.filter((entry) => needed.has(entry.id));
|
const storage = attachments.filter((entry) => needed.has(entry.id));
|
||||||
const bindingDigest = createHash("sha256")
|
const bindingDigest = createHash("sha256")
|
||||||
.update(JSON.stringify({ definition: checked.definitionDigest, fields, storage }))
|
.update(
|
||||||
|
canonicalJson({
|
||||||
|
definition: checked.definitionDigest,
|
||||||
|
fields: [...fields].sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b))),
|
||||||
|
storage: [...storage].sort((a, b) => a.id.localeCompare(b.id)),
|
||||||
|
}),
|
||||||
|
)
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
linked.push({
|
linked.push({
|
||||||
id: `${pkg.revisionId}:${declaration.id}`,
|
id: `${pkg.revisionId}:${declaration.id}`,
|
||||||
@@ -140,5 +147,10 @@ export function linkQueries(workspace: WorkspaceRevision): LinkedQuery[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const entry of linked) entry.runtime = queryRuntimePlan(entry, workspace);
|
for (const entry of linked) entry.runtime = queryRuntimePlan(entry, workspace);
|
||||||
|
if (new Set(linked.map((entry) => entry.id)).size !== linked.length)
|
||||||
|
throw new QueryCompileError(
|
||||||
|
"QUERY_ID_COLLISION",
|
||||||
|
"Query export identities collide; choose distinct package/query IDs",
|
||||||
|
);
|
||||||
return linked;
|
return linked;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export const queryRuntimePlan = (linked: LinkedQuery, workspace: WorkspaceRevisi
|
|||||||
memberId: field.memberId,
|
memberId: field.memberId,
|
||||||
getterOperationId: field.getter,
|
getterOperationId: field.getter,
|
||||||
fieldName: member.displayName,
|
fieldName: member.displayName,
|
||||||
|
keyType: member.kind === "relationship" ? member.keyType : undefined,
|
||||||
valueTypeJson: member.kind === "value" ? JSON.stringify(member.valueType) : "",
|
valueTypeJson: member.kind === "value" ? JSON.stringify(member.valueType) : "",
|
||||||
cardinality:
|
cardinality:
|
||||||
member.kind === "relationship"
|
member.kind === "relationship"
|
||||||
|
|||||||
+11
-1
@@ -203,6 +203,15 @@ export function querySchema(declaration: QueryDeclaration, interfaces: readonly
|
|||||||
name: `${name(id)}_${member.displayName}_Entry`,
|
name: `${name(id)}_${member.displayName}_Entry`,
|
||||||
fields: {
|
fields: {
|
||||||
key: { type: new GraphQLNonNull(GraphQLString) },
|
key: { type: new GraphQLNonNull(GraphQLString) },
|
||||||
|
...(member.keyType
|
||||||
|
? {
|
||||||
|
mapKey: {
|
||||||
|
type: new GraphQLNonNull(
|
||||||
|
queryScalars[member.keyType === "boolean" ? "bool" : member.keyType],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
cursor: { type: cursorScalar },
|
cursor: { type: cursorScalar },
|
||||||
node: { type: new GraphQLNonNull(node) },
|
node: { type: new GraphQLNonNull(node) },
|
||||||
},
|
},
|
||||||
@@ -218,7 +227,8 @@ export function querySchema(declaration: QueryDeclaration, interfaces: readonly
|
|||||||
fields[member.displayName] = {
|
fields[member.displayName] = {
|
||||||
type: new GraphQLNonNull(connection),
|
type: new GraphQLNonNull(connection),
|
||||||
args: {
|
args: {
|
||||||
first: { type: new GraphQLNonNull(GraphQLInt) },
|
first: { type: GraphQLInt },
|
||||||
|
all: { type: GraphQLInt },
|
||||||
after: { type: cursorScalar },
|
after: { type: cursorScalar },
|
||||||
where: { type: filter(targetId) },
|
where: { type: filter(targetId) },
|
||||||
...(ordering ? { orderBy: { type: new GraphQLList(new GraphQLNonNull(ordering)) } } : {}),
|
...(ordering ? { orderBy: { type: new GraphQLList(new GraphQLNonNull(ordering)) } } : {}),
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { capabilityId, type InterfaceRevision, type InterfaceRevisionId } from "../capability-model/types.js";
|
||||||
|
import {
|
||||||
|
TypeSubstitution,
|
||||||
|
bindTypeParameters,
|
||||||
|
type ClosedTypeArgument,
|
||||||
|
type GenericTypeEnvironment,
|
||||||
|
} from "../capability-model/generics.js";
|
||||||
|
import { GenericSourceTypes } from "../capability-language/generic-types.js";
|
||||||
|
import { compileQuery } from "./compile.js";
|
||||||
|
import { QueryCompileError, type QueryDeclaration, type QueryTemplate } from "./types.js";
|
||||||
|
|
||||||
|
export function specializeQueryTemplate(
|
||||||
|
template: QueryTemplate,
|
||||||
|
arguments_: ClosedTypeArgument[],
|
||||||
|
environment: GenericTypeEnvironment,
|
||||||
|
interfaces: () => readonly InterfaceRevision[],
|
||||||
|
identity: { id: string; displayName: string },
|
||||||
|
): QueryDeclaration {
|
||||||
|
const obligations: NonNullable<QueryDeclaration["argumentRequirements"]> = [];
|
||||||
|
const checked = {
|
||||||
|
...environment,
|
||||||
|
implementsInterface: (
|
||||||
|
target: Parameters<GenericTypeEnvironment["implementsInterface"]>[0],
|
||||||
|
required: InterfaceRevisionId,
|
||||||
|
) => {
|
||||||
|
obligations.push({ target, required });
|
||||||
|
return environment.implementsInterface(target, required);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const substitution = new TypeSubstitution({
|
||||||
|
...checked,
|
||||||
|
arguments: bindTypeParameters(template.parameters, arguments_, checked, template.declaration.displayName),
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...structuredClone(template.declaration),
|
||||||
|
...identity,
|
||||||
|
root: substitution.application(template.root),
|
||||||
|
views: template.views
|
||||||
|
.map((view) => {
|
||||||
|
const target = substitution.object(view.target);
|
||||||
|
const interfaceRevisionId = substitution.application(view.interface);
|
||||||
|
if (target.kind === "interface") {
|
||||||
|
if (target.interfaceRevisionId !== interfaceRevisionId)
|
||||||
|
throw new QueryCompileError(
|
||||||
|
"QUERY_VIEW_REQUIRED",
|
||||||
|
"An interface argument must use its exact selected query view",
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return { atomId: target.atomId, interfaceRevisionId };
|
||||||
|
})
|
||||||
|
.filter((view): view is NonNullable<typeof view> => !!view),
|
||||||
|
allowances: template.allowances.map((allowance) => {
|
||||||
|
const interfaceRevisionId = substitution.application(allowance.interface);
|
||||||
|
const member = interfaces()
|
||||||
|
.find((entry) => entry.revisionId === interfaceRevisionId)
|
||||||
|
?.members.find((entry) => entry.displayName === allowance.memberName);
|
||||||
|
if (!member) throw new QueryCompileError("QUERY_ALLOWANCE", `Unknown template field ${allowance.memberName}`);
|
||||||
|
return { interfaceRevisionId, memberId: member.id, uses: allowance.uses, reason: allowance.reason };
|
||||||
|
}),
|
||||||
|
application: { templateId: template.declaration.id, arguments: structuredClone(arguments_) },
|
||||||
|
argumentRequirements: obligations,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check a document with only its declared bounds available, even if no concrete
|
||||||
|
* specialization is installed. No concrete atom fields can leak into this check. */
|
||||||
|
export async function checkQueryTemplate(
|
||||||
|
template: QueryTemplate,
|
||||||
|
interfaces: readonly InterfaceRevision[],
|
||||||
|
read: (name: string) => Promise<string>,
|
||||||
|
) {
|
||||||
|
const types = new GenericSourceTypes(new Map());
|
||||||
|
for (const contract of interfaces) types.register(contract.displayName, contract);
|
||||||
|
const arguments_: ClosedTypeArgument[] = template.parameters.map((parameter) => {
|
||||||
|
if (parameter.kind !== "object")
|
||||||
|
throw new QueryCompileError(
|
||||||
|
"QUERY_TEMPLATE_PARAMETER",
|
||||||
|
"Query templates currently require object parameters with explicit interface views; scalar/value polymorphism is not queryable",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
kind: "object",
|
||||||
|
target: { kind: "atom", atomId: capabilityId.atom(`query-bound:${template.declaration.id}:${parameter.id}`) },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const bindings = new Map(template.parameters.map((parameter, index) => [parameter.id, arguments_[index]!]));
|
||||||
|
const substitution = new TypeSubstitution({ ...types.environment(), arguments: bindings });
|
||||||
|
const evidence = new Map(
|
||||||
|
arguments_.map((argument, index) => {
|
||||||
|
const parameter = template.parameters[index]!;
|
||||||
|
const target = argument.kind === "object" && argument.target.kind === "atom" ? argument.target.atomId : "";
|
||||||
|
return [
|
||||||
|
target,
|
||||||
|
new Set(
|
||||||
|
parameter.kind === "object" ? parameter.implements.map((bound) => substitution.application(bound)) : [],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const implies = (offered: string, required: string, seen = new Set<string>()): boolean => {
|
||||||
|
if (offered === required) return true;
|
||||||
|
if (seen.has(offered)) return false;
|
||||||
|
seen.add(offered);
|
||||||
|
const contract = types.definitions.get(offered);
|
||||||
|
return (contract?.requiredInterfaces ?? []).some((parent) => implies(parent, required, seen));
|
||||||
|
};
|
||||||
|
const environment = {
|
||||||
|
...types.environment(),
|
||||||
|
implementsInterface: (
|
||||||
|
target: Parameters<GenericTypeEnvironment["implementsInterface"]>[0],
|
||||||
|
required: InterfaceRevisionId,
|
||||||
|
) =>
|
||||||
|
target.kind === "atom" && [...(evidence.get(target.atomId) ?? [])].some((offered) => implies(offered, required)),
|
||||||
|
};
|
||||||
|
const declaration = specializeQueryTemplate(
|
||||||
|
template,
|
||||||
|
arguments_,
|
||||||
|
environment,
|
||||||
|
() => [...types.definitions.values()],
|
||||||
|
template.declaration,
|
||||||
|
);
|
||||||
|
for (const view of declaration.views)
|
||||||
|
if (!environment.implementsInterface({ kind: "atom", atomId: view.atomId }, view.interfaceRevisionId))
|
||||||
|
throw new QueryCompileError(
|
||||||
|
"QUERY_TEMPLATE_BOUND",
|
||||||
|
`The declared bounds do not guarantee the selected view ${view.interfaceRevisionId}`,
|
||||||
|
);
|
||||||
|
for (const application of types.applications.values())
|
||||||
|
for (const obligation of application.argumentRequirements ?? [])
|
||||||
|
if (!environment.implementsInterface(obligation.target, obligation.required))
|
||||||
|
throw new QueryCompileError(
|
||||||
|
"QUERY_TEMPLATE_BOUND",
|
||||||
|
`Template arguments do not guarantee ${obligation.required}`,
|
||||||
|
);
|
||||||
|
return compileQuery(declaration, [...types.definitions.values()], read);
|
||||||
|
}
|
||||||
+20
-1
@@ -1,4 +1,10 @@
|
|||||||
import type { AtomId, InterfaceRevisionId, MemberId, ValueType } from "../capability-model/types.js";
|
import type { AtomId, InterfaceRevisionId, MemberId, ValueType } from "../capability-model/types.js";
|
||||||
|
import type {
|
||||||
|
TypeParameter,
|
||||||
|
InterfaceApplicationExpression,
|
||||||
|
ObjectTypeExpression,
|
||||||
|
ClosedTypeArgument,
|
||||||
|
} from "../capability-model/generics.js";
|
||||||
|
|
||||||
export type QueryUse = "select" | "predicate" | "order";
|
export type QueryUse = "select" | "predicate" | "order";
|
||||||
export interface QueryAllowance {
|
export interface QueryAllowance {
|
||||||
@@ -37,6 +43,19 @@ export interface QueryDeclaration {
|
|||||||
budgets: QueryBudgets;
|
budgets: QueryBudgets;
|
||||||
watch: boolean;
|
watch: boolean;
|
||||||
polling?: { intervalMs: number; reason: string };
|
polling?: { intervalMs: number; reason: string };
|
||||||
|
application?: { templateId: string; arguments: ClosedTypeArgument[] };
|
||||||
|
argumentRequirements?: {
|
||||||
|
target: import("../capability-model/types.js").ObjectExpectation;
|
||||||
|
required: InterfaceRevisionId;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
/** Source-only template; neither parameters nor synthetic bound witnesses are installed. */
|
||||||
|
export interface QueryTemplate {
|
||||||
|
declaration: Omit<QueryDeclaration, "root" | "views" | "allowances">;
|
||||||
|
parameters: TypeParameter[];
|
||||||
|
root: InterfaceApplicationExpression;
|
||||||
|
views: { target: ObjectTypeExpression; interface: InterfaceApplicationExpression }[];
|
||||||
|
allowances: { interface: InterfaceApplicationExpression; memberName: string; uses: QueryUse[]; reason: string }[];
|
||||||
}
|
}
|
||||||
export interface QueryFieldEffect {
|
export interface QueryFieldEffect {
|
||||||
interfaceRevisionId: InterfaceRevisionId;
|
interfaceRevisionId: InterfaceRevisionId;
|
||||||
@@ -55,7 +74,7 @@ export class QueryCompileError extends Error {
|
|||||||
message: string,
|
message: string,
|
||||||
readonly location?: QuerySourceLocation,
|
readonly location?: QuerySourceLocation,
|
||||||
) {
|
) {
|
||||||
super(message);
|
super(`${code}${location ? ` ${location.file}:${location.line}:${location.column}` : ""}: ${message}`);
|
||||||
this.name = "QueryCompileError";
|
this.name = "QueryCompileError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+117
@@ -0,0 +1,117 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { compileCapabilityResourceSource, compileCapabilitySource } from "../../src/capability-language/index.js";
|
||||||
|
import { compileQuery } from "../../src/query/compile.js";
|
||||||
|
import { linkQueries } from "../../src/query/link.js";
|
||||||
|
import type { InterfaceRevision, PackageRevision } from "../../src/capability-model/types.js";
|
||||||
|
|
||||||
|
export const queryFixtureSource = {
|
||||||
|
repository: "https://query-fixture.example.test/source.git",
|
||||||
|
commit: "1".repeat(40),
|
||||||
|
};
|
||||||
|
export async function queryWorkspaceFixture() {
|
||||||
|
const interfaces = new Map<string, InterfaceRevision>();
|
||||||
|
const sources: Record<string, string> = {};
|
||||||
|
function iface(name: string, members: string, parameters = "") {
|
||||||
|
const text = `${[...interfaces.keys()].map((name) => `import interface ${name};`).join("\n")}
|
||||||
|
interface ${name}${parameters} id "${name}" revision "${name}@1" {${members}}`;
|
||||||
|
sources[name] = text;
|
||||||
|
const result = compileCapabilityResourceSource(text, {
|
||||||
|
source: queryFixtureSource,
|
||||||
|
environment: { interfaces, interfaceClosure: [...interfaces.values()] },
|
||||||
|
});
|
||||||
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||||
|
if (result.resource.kind !== "interface") throw new Error("interface expected");
|
||||||
|
interfaces.set(name, result.resource.revision);
|
||||||
|
}
|
||||||
|
iface("PersonFacts", `queryable value name id "name" : string {get id "name:get";}`);
|
||||||
|
iface(
|
||||||
|
"TaskFacts",
|
||||||
|
`
|
||||||
|
queryable value title id "title" : string {get id "title:get"; set id "title:set";}
|
||||||
|
queryable value done id "done" : bool {get id "done:get";}
|
||||||
|
queryable value rank id "rank" : int64 {get id "rank:get";}
|
||||||
|
queryable relation assignee id "assignee" : optional-one interface PersonFacts {resolve id "assignee:resolve";}
|
||||||
|
queryable rpc value score id "score" : int32 {get id "score:get";}
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
iface(
|
||||||
|
"Collection",
|
||||||
|
`queryable relation items id "items" : many object Item ordered {resolve id "items:resolve";}`,
|
||||||
|
"<object Item implements TaskFacts>",
|
||||||
|
);
|
||||||
|
const packageSource = `${[...interfaces.keys()].map((name) => `import interface ${name};`).join("\n")}
|
||||||
|
package Queries id "queries" revision "queries@1" {
|
||||||
|
operation titleSet id "title-set" : string -> unit mode call receiver any
|
||||||
|
requires {state title id "title-port" : string [write];};
|
||||||
|
operation score id "score" : unit -> int32 mode call receiver any;
|
||||||
|
query Upcoming id "upcoming" root Collection<interface TaskFacts> document "upcoming.graphql" operation "Upcoming" {
|
||||||
|
fragments "row.graphql"; max rows 30; watch;
|
||||||
|
}
|
||||||
|
query Enriched id "enriched" root Collection<interface TaskFacts> document "enriched.graphql" operation "Enriched" {
|
||||||
|
max rows 30; allow TaskFacts.score select "Small visible page only";
|
||||||
|
}
|
||||||
|
query Ranked id "ranked" root Collection<interface TaskFacts> document "ranked.graphql" operation "Ranked" {
|
||||||
|
max rows 30; max candidates 60;
|
||||||
|
allow TaskFacts.score predicate "Bounded local collection";
|
||||||
|
allow TaskFacts.score order "Bounded local collection";
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
sources.Queries = packageSource;
|
||||||
|
const pkgResult = compileCapabilityResourceSource(packageSource, {
|
||||||
|
source: queryFixtureSource,
|
||||||
|
environment: { interfaces, interfaceClosure: [...interfaces.values()] },
|
||||||
|
});
|
||||||
|
assert.ok(pkgResult.ok, JSON.stringify(pkgResult.diagnostics));
|
||||||
|
if (pkgResult.resource.kind !== "package") throw new Error("package expected");
|
||||||
|
const pkg: PackageRevision = pkgResult.resource.revision;
|
||||||
|
const allInterfaces = [...interfaces.values(), ...(pkgResult.resource.specializations ?? [])];
|
||||||
|
const documents: Record<string, string> = {
|
||||||
|
"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}}}}`,
|
||||||
|
"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}}}}}`,
|
||||||
|
};
|
||||||
|
pkg.checkedQueries = await Promise.all(
|
||||||
|
pkg.queries!.map((query) => compileQuery(query, allInterfaces, async (name) => documents[name]!)),
|
||||||
|
);
|
||||||
|
const implementation = (atom: string) => `conform ${atom} as TaskFacts id "${atom}-facts" {
|
||||||
|
private state Title${atom} id "${atom}:title" on ${atom} : string policy crdt(string) default "Untitled";
|
||||||
|
private state Done${atom} id "${atom}:done" on ${atom} : bool policy optimistic-register default false;
|
||||||
|
private state Rank${atom} id "${atom}:rank" on ${atom} : int64 policy optimistic-register default 0;
|
||||||
|
private edge Assignee${atom} id "${atom}:assignee" {
|
||||||
|
atom ${atom} projection assignee id "${atom}:assignee:forward" optional-one;
|
||||||
|
interface PersonFacts projection tasks id "${atom}:assignee:inverse" many;
|
||||||
|
}
|
||||||
|
bind title.get to state Title${atom}.read;
|
||||||
|
bind title.set to package Queries.titleSet with {title to state Title${atom};};
|
||||||
|
bind done to state Done${atom}; bind rank to state Rank${atom};
|
||||||
|
bind assignee.resolve to edge Assignee${atom}.assignee.resolve;
|
||||||
|
bind score.get to package Queries.score query-reason "Explicit bounded score computation";
|
||||||
|
}`;
|
||||||
|
const workspaceSource = `workspace QueryFixture id "query-fixture" revision "query-fixture@1" commit "${queryFixtureSource.commit}" {
|
||||||
|
import interface PersonFacts; import interface TaskFacts; import interface Collection; import package Queries;
|
||||||
|
atom Task id "Task"; atom Reminder id "Reminder"; atom Person id "Person"; atom Tasks id "Tasks";
|
||||||
|
conform Person as PersonFacts id "person-facts" {
|
||||||
|
private state Name id "person:name" on Person : string policy optimistic-register default "Nobody";
|
||||||
|
bind name to state Name;
|
||||||
|
}
|
||||||
|
${implementation("Task")} ${implementation("Reminder")}
|
||||||
|
conform Tasks as Collection<interface TaskFacts> id "tasks-collection" {
|
||||||
|
private edge Items id "items" {
|
||||||
|
atom Tasks projection items id "items:forward" many ordered;
|
||||||
|
interface TaskFacts projection collections id "items:inverse" many;
|
||||||
|
}
|
||||||
|
bind items.resolve to edge Items.items.resolve;
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
sources.workspace = workspaceSource;
|
||||||
|
const result = compileCapabilitySource(workspaceSource, "workspace.qx", {
|
||||||
|
interfaces,
|
||||||
|
interfaceClosure: allInterfaces,
|
||||||
|
packages: new Map([["Queries", pkg]]),
|
||||||
|
packageClosure: [pkg],
|
||||||
|
});
|
||||||
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||||
|
const workspace = { ...result.workspace, linkedQueries: linkQueries(result.workspace) };
|
||||||
|
return { workspace, pkg, interfaces: allInterfaces, sources, documents };
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { test } from "node:test";
|
|||||||
import { compileCapabilityResourceSource } from "../src/capability-language/index.js";
|
import { compileCapabilityResourceSource } from "../src/capability-language/index.js";
|
||||||
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 type { InterfaceRevision } from "../src/capability-model/types.js";
|
import type { InterfaceRevision } from "../src/capability-model/types.js";
|
||||||
|
|
||||||
const source = { repository: "https://example.test/queries.git", commit: "a".repeat(40) };
|
const source = { repository: "https://example.test/queries.git", commit: "a".repeat(40) };
|
||||||
@@ -33,6 +34,64 @@ interface Tasks id "tasks" revision "tasks@1" {
|
|||||||
}`,
|
}`,
|
||||||
[facts],
|
[facts],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test("generic query templates check declared bounds before closed specialization", async () => {
|
||||||
|
const generic = iface(
|
||||||
|
`import interface TaskFacts;
|
||||||
|
interface Collection<object Item implements TaskFacts> id "generic-collection" revision "generic-collection@1" {
|
||||||
|
queryable relation items id "items" : many object Item { resolve id "items:resolve"; }
|
||||||
|
}`,
|
||||||
|
[facts],
|
||||||
|
);
|
||||||
|
const build = (bound: string) =>
|
||||||
|
compileCapabilityResourceSource(
|
||||||
|
`import interface Collection; import interface TaskFacts;
|
||||||
|
package GenericQueries id "generic-queries" revision "generic-queries@1" {
|
||||||
|
query Rows<object Item ${bound}> id "rows-template" root Collection<Item>
|
||||||
|
document "rows.graphql" operation "Rows" { view object Item as TaskFacts; max rows 30; }
|
||||||
|
query TaskRows id "task-rows" specialize Rows<interface TaskFacts>;
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
source,
|
||||||
|
environment: {
|
||||||
|
interfaces: new Map([
|
||||||
|
["Collection", generic],
|
||||||
|
["TaskFacts", facts],
|
||||||
|
]),
|
||||||
|
interfaceClosure: [generic, facts],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const result = build("implements TaskFacts");
|
||||||
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||||
|
if (result.resource.kind !== "package") throw new Error("package expected");
|
||||||
|
const pkg = result.resource.revision;
|
||||||
|
assert.equal(pkg.queries?.length, 1);
|
||||||
|
assert.equal(pkg.queryTemplates?.length, 1);
|
||||||
|
assert.equal(pkg.queries![0]!.application!.templateId, "rows-template");
|
||||||
|
const read = async () => `query Rows { root { items(first: 3) { entries { node { title } } } } }`;
|
||||||
|
const universal = await checkQueryTemplate(pkg.queryTemplates![0]!, [generic, facts], read);
|
||||||
|
assert.equal(
|
||||||
|
universal.effects.some((effect) => effect.memberId === "title"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const specialized = await compileQuery(
|
||||||
|
pkg.queries![0]!,
|
||||||
|
[generic, facts, ...(result.resource.specializations ?? [])],
|
||||||
|
read,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
specialized.effects.some((effect) => effect.memberId === "title"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const invalid = build("");
|
||||||
|
assert.ok(invalid.ok, JSON.stringify(invalid.diagnostics));
|
||||||
|
if (invalid.resource.kind !== "package") throw new Error("package expected");
|
||||||
|
await assert.rejects(
|
||||||
|
checkQueryTemplate(invalid.resource.revision.queryTemplates![0]!, [generic, facts], read),
|
||||||
|
/bound|guarantee|implement/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
function fixture(clauses = "") {
|
function fixture(clauses = "") {
|
||||||
const result = compileCapabilityResourceSource(
|
const result = compileCapabilityResourceSource(
|
||||||
`import interface Tasks; import interface TaskFacts;
|
`import interface Tasks; import interface TaskFacts;
|
||||||
@@ -66,6 +125,40 @@ const document = `query Upcoming($first: Int!, $before: Int64!) {
|
|||||||
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("query dependency ports resolve exact exports without declaration ordering constraints", () => {
|
||||||
|
const result = compileCapabilityResourceSource(
|
||||||
|
`import interface Tasks;
|
||||||
|
package Queries id "queries" revision "queries@1" {
|
||||||
|
operation load id "read" : unit -> unit mode call receiver interfaces [Tasks]
|
||||||
|
requires { query upcoming id "upcoming-port" : Queries.Upcoming; };
|
||||||
|
query Upcoming id "upcoming" root Tasks document "upcoming.graphql" operation "Upcoming" { max rows 30; }
|
||||||
|
}`,
|
||||||
|
{ source, environment: { interfaces: new Map([["Tasks", collection]]), interfaceClosure: [collection, facts] } },
|
||||||
|
);
|
||||||
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||||
|
assert.equal(result.resource.kind, "package");
|
||||||
|
if (result.resource.kind !== "package") throw new Error("package expected");
|
||||||
|
assert.deepEqual(result.resource.revision.exports[0]!.dependencyPorts[0]!.requirement, {
|
||||||
|
kind: "query",
|
||||||
|
packageRevisionId: "queries@1",
|
||||||
|
queryId: "upcoming",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("result types merge repeated selections and preserve conditional fragment fields", async () => {
|
||||||
|
const checked = await compile(
|
||||||
|
`query Upcoming($show: Boolean!) {
|
||||||
|
root { items(first: 3) { entries { node { title } } } }
|
||||||
|
root { items(first: 3) { entries { node { ...Row @include(if: $show) } } } }
|
||||||
|
}`,
|
||||||
|
"fragment Row on TaskFacts { due done }",
|
||||||
|
);
|
||||||
|
const output = JSON.stringify(checked.output);
|
||||||
|
assert.match(output, /\"title\":\{\"kind\":\"scalar\",\"name\":\"string\"\}/);
|
||||||
|
assert.match(output, /\"done\":\{\"kind\":\"optional\",\"value\":\{\"kind\":\"scalar\",\"name\":\"bool\"\}\}/);
|
||||||
|
assert.match(output, /\"due\"/);
|
||||||
|
});
|
||||||
|
|
||||||
test("fixed GraphQL yields exact effects, typed references and distinct query artifacts", async () => {
|
test("fixed GraphQL yields exact effects, typed references and distinct query artifacts", async () => {
|
||||||
const checked = await compile();
|
const checked = await compile();
|
||||||
assert.deepEqual(checked.variables, {
|
assert.deepEqual(checked.variables, {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import { queryWorkspaceFixture } from "./fixtures/query-workspace.js";
|
||||||
|
import { linkQueries } from "../src/query/link.js";
|
||||||
|
|
||||||
|
test("query fixture links generic collections, both implementations, and native getter/RPC setter", async () => {
|
||||||
|
const { workspace } = await queryWorkspaceFixture();
|
||||||
|
const query = workspace.linkedQueries.find((query) => query.id.endsWith(":upcoming"))!;
|
||||||
|
assert.ok(query);
|
||||||
|
assert.equal(query.fields.filter((field) => field.memberId === "title").length, 2);
|
||||||
|
assert.ok(
|
||||||
|
query.fields.filter((field) => field.memberId === "title").every((field) => field.binding.kind === "state"),
|
||||||
|
);
|
||||||
|
const broken = structuredClone(workspace);
|
||||||
|
const facts = broken.interfaceImports.find((iface) => iface.displayName === "TaskFacts")!;
|
||||||
|
const title = facts.members.find((member) => member.id === "title")!;
|
||||||
|
assert.equal(title.kind, "value");
|
||||||
|
delete title.queryRead;
|
||||||
|
assert.throws(() => linkQueries(broken), /changed/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user