Support checked keyed relationships across portable operations and queries
This commit is contained in:
@@ -245,7 +245,7 @@ dependencyBlock
|
|||||||
dependencyPort
|
dependencyPort
|
||||||
: REFLECTION identifier ID stringLiteral LBRACK identifierList RBRACK SEMI
|
: REFLECTION identifier ID stringLiteral LBRACK identifierList RBRACK SEMI
|
||||||
| STATE identifier ID stringLiteral COLON valueType primitiveList SEMI
|
| STATE identifier ID stringLiteral COLON valueType primitiveList SEMI
|
||||||
| EDGE identifier ID stringLiteral COLON cardinality targetConstraint primitiveList SEMI
|
| EDGE identifier ID stringLiteral COLON cardinality targetConstraint (KEYED stringLiteral)? primitiveList SEMI
|
||||||
| INTERFACE identifier ID stringLiteral COLON identifier typeArguments? selectedOperations? (FROM INPUT)? SEMI
|
| INTERFACE identifier ID stringLiteral COLON identifier typeArguments? selectedOperations? (FROM INPUT)? SEMI
|
||||||
| CREATION identifier ID stringLiteral COLON identifier INPUT valueType SEMI
|
| CREATION identifier ID stringLiteral COLON identifier INPUT valueType SEMI
|
||||||
| CONSTRUCTOR identifier ID stringLiteral COLON identifier (INPUT valueType)? SEMI
|
| CONSTRUCTOR identifier ID stringLiteral COLON identifier (INPUT valueType)? SEMI
|
||||||
|
|||||||
@@ -163,12 +163,16 @@ function methods(
|
|||||||
});
|
});
|
||||||
case "edge": {
|
case "edge": {
|
||||||
const ref: ValueType = { kind: "object-ref", expectation: req.target };
|
const ref: ValueType = { kind: "object-ref", expectation: req.target };
|
||||||
const item: ValueType = { kind: "record", fields: { entry: string, target: ref } };
|
const mapKey: ValueType | undefined = req.keyType
|
||||||
|
? { kind: "scalar", name: req.keyType === "boolean" ? "bool" : req.keyType }
|
||||||
|
: undefined;
|
||||||
|
const item: ValueType = { kind: "record", fields: { entry: string, target: ref, ...(mapKey ? { mapKey } : {}) } };
|
||||||
|
const connectInput: ValueType = mapKey ? { kind: "record", fields: { mapKey, target: ref } } : ref;
|
||||||
return req.primitives.map((primitive) => {
|
return req.primitives.map((primitive) => {
|
||||||
if (primitive === "resolve")
|
if (primitive === "resolve")
|
||||||
return method("resolve", { kind: "edge", primitive }, unit, { kind: "list", value: item });
|
return method("resolve", { kind: "edge", primitive }, unit, { kind: "list", value: item });
|
||||||
writable();
|
writable();
|
||||||
if (primitive === "connect") return method("connect", { kind: "edge", primitive }, ref, string, true);
|
if (primitive === "connect") return method("connect", { kind: "edge", primitive }, connectInput, string, true);
|
||||||
if (primitive === "disconnect") return method("disconnect", { kind: "edge", primitive }, string, bool);
|
if (primitive === "disconnect") return method("disconnect", { kind: "edge", primitive }, string, bool);
|
||||||
throw new PortableBindingError(path, `Edge ${primitive} is not supported by this portable adapter yet`);
|
throw new PortableBindingError(path, `Edge ${primitive} is not supported by this portable adapter yet`);
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -445,6 +445,7 @@ const relationshipOperation = (
|
|||||||
id: InterfaceOperation["id"],
|
id: InterfaceOperation["id"],
|
||||||
target: EdgeEndpointConstraint,
|
target: EdgeEndpointConstraint,
|
||||||
cardinality: EdgeCardinality,
|
cardinality: EdgeCardinality,
|
||||||
|
keyType?: "string" | "boolean" | "int64",
|
||||||
): InterfaceOperation => {
|
): InterfaceOperation => {
|
||||||
const targetType =
|
const targetType =
|
||||||
target.kind === "atom" ? valueType.atomRef(target.atomId) : valueType.interfaceRef(target.interfaceRevisionId);
|
target.kind === "atom" ? valueType.atomRef(target.atomId) : valueType.interfaceRef(target.interfaceRevisionId);
|
||||||
@@ -463,7 +464,16 @@ const relationshipOperation = (
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
displayName,
|
displayName,
|
||||||
inputType: targetType,
|
inputType:
|
||||||
|
keyType && displayName === "connect"
|
||||||
|
? {
|
||||||
|
kind: "record",
|
||||||
|
fields: {
|
||||||
|
mapKey: { kind: "scalar", name: keyType === "boolean" ? "bool" : keyType },
|
||||||
|
target: targetType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: targetType,
|
||||||
outputType: valueType.unit,
|
outputType: valueType.unit,
|
||||||
mode: "call",
|
mode: "call",
|
||||||
};
|
};
|
||||||
@@ -545,6 +555,9 @@ const lowerRelationshipMember = (
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const cardinality = lowerCardinality(context.cardinality());
|
const cardinality = lowerCardinality(context.cardinality());
|
||||||
|
const keyType = context.KEYED()
|
||||||
|
? (stringValue(context.stringLiteral(1)) as "string" | "boolean" | "int64")
|
||||||
|
: undefined;
|
||||||
const operations: InterfaceOperation[] = [];
|
const operations: InterfaceOperation[] = [];
|
||||||
for (const operation of context.relationshipOperation()) {
|
for (const operation of context.relationshipOperation()) {
|
||||||
if (operation.RESOLVE()) {
|
if (operation.RESOLVE()) {
|
||||||
@@ -554,6 +567,7 @@ const lowerRelationshipMember = (
|
|||||||
capabilityId.operation(stringValue(operation.stringLiteral(0))),
|
capabilityId.operation(stringValue(operation.stringLiteral(0))),
|
||||||
target,
|
target,
|
||||||
cardinality,
|
cardinality,
|
||||||
|
keyType,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (operation.CONNECT()) {
|
} else if (operation.CONNECT()) {
|
||||||
@@ -563,6 +577,7 @@ const lowerRelationshipMember = (
|
|||||||
capabilityId.operation(stringValue(operation.stringLiteral(0))),
|
capabilityId.operation(stringValue(operation.stringLiteral(0))),
|
||||||
target,
|
target,
|
||||||
cardinality,
|
cardinality,
|
||||||
|
keyType,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (operation.DISCONNECT()) {
|
} else if (operation.DISCONNECT()) {
|
||||||
@@ -572,6 +587,7 @@ const lowerRelationshipMember = (
|
|||||||
capabilityId.operation(stringValue(operation.stringLiteral(0))),
|
capabilityId.operation(stringValue(operation.stringLiteral(0))),
|
||||||
target,
|
target,
|
||||||
cardinality,
|
cardinality,
|
||||||
|
keyType,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -581,12 +597,14 @@ const lowerRelationshipMember = (
|
|||||||
capabilityId.operation(stringValue(operation.stringLiteral(0))),
|
capabilityId.operation(stringValue(operation.stringLiteral(0))),
|
||||||
target,
|
target,
|
||||||
cardinality,
|
cardinality,
|
||||||
|
keyType,
|
||||||
),
|
),
|
||||||
relationshipOperation(
|
relationshipOperation(
|
||||||
"watch-stop",
|
"watch-stop",
|
||||||
capabilityId.operation(stringValue(operation.stringLiteral(1))),
|
capabilityId.operation(stringValue(operation.stringLiteral(1))),
|
||||||
target,
|
target,
|
||||||
cardinality,
|
cardinality,
|
||||||
|
keyType,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -847,6 +865,15 @@ const lowerInterfaceTemplate = (
|
|||||||
const target = types.targetExpression(relationship.targetConstraint());
|
const target = types.targetExpression(relationship.targetConstraint());
|
||||||
const targetType: ValueTypeExpression = { kind: "object-ref", expectation: target };
|
const targetType: ValueTypeExpression = { kind: "object-ref", expectation: target };
|
||||||
const cardinality = lowerCardinality(relationship.cardinality());
|
const cardinality = lowerCardinality(relationship.cardinality());
|
||||||
|
const keyType = relationship.KEYED()
|
||||||
|
? (stringValue(relationship.stringLiteral(1)) as "string" | "boolean" | "int64")
|
||||||
|
: undefined;
|
||||||
|
const keyedTarget: ValueTypeExpression = keyType
|
||||||
|
? {
|
||||||
|
kind: "record",
|
||||||
|
fields: { mapKey: { kind: "scalar", name: keyType === "boolean" ? "bool" : keyType }, target: targetType },
|
||||||
|
}
|
||||||
|
: targetType;
|
||||||
const resolved: ValueTypeExpression =
|
const resolved: ValueTypeExpression =
|
||||||
cardinality === "exactly-one"
|
cardinality === "exactly-one"
|
||||||
? targetType
|
? targetType
|
||||||
@@ -872,6 +899,7 @@ const lowerInterfaceTemplate = (
|
|||||||
target,
|
target,
|
||||||
cardinality,
|
cardinality,
|
||||||
ordered: Boolean(relationship.ORDERED()),
|
ordered: Boolean(relationship.ORDERED()),
|
||||||
|
...(keyType ? { keyType } : {}),
|
||||||
operations: relationship.relationshipOperation().flatMap((operation) => {
|
operations: relationship.relationshipOperation().flatMap((operation) => {
|
||||||
const id = capabilityId.operation(stringValue(operation.stringLiteral(0)));
|
const id = capabilityId.operation(stringValue(operation.stringLiteral(0)));
|
||||||
if (operation.RESOLVE())
|
if (operation.RESOLVE())
|
||||||
@@ -881,7 +909,12 @@ const lowerInterfaceTemplate = (
|
|||||||
if (operation.CONNECT() || operation.DISCONNECT())
|
if (operation.CONNECT() || operation.DISCONNECT())
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
...signature(operation.CONNECT() ? "connect" : "disconnect", id, targetType, valueType.unit),
|
...signature(
|
||||||
|
operation.CONNECT() ? "connect" : "disconnect",
|
||||||
|
id,
|
||||||
|
operation.CONNECT() ? keyedTarget : targetType,
|
||||||
|
valueType.unit,
|
||||||
|
),
|
||||||
...lowerExecution(operation.executionClause()),
|
...lowerExecution(operation.executionClause()),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -957,7 +990,7 @@ const lowerQueryReference = (
|
|||||||
|
|
||||||
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(0)));
|
||||||
if (context.REFLECTION())
|
if (context.REFLECTION())
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -1016,6 +1049,9 @@ const lowerDependencyPort = (state: LoweringState, context: DependencyPortContex
|
|||||||
kind: "edge",
|
kind: "edge",
|
||||||
target,
|
target,
|
||||||
cardinality: lowerCardinality(context.cardinality()),
|
cardinality: lowerCardinality(context.cardinality()),
|
||||||
|
...(context.KEYED()
|
||||||
|
? { keyType: stringValue(context.stringLiteral(1)) as "string" | "boolean" | "int64" }
|
||||||
|
: {}),
|
||||||
primitives: primitives.filter((entry) => !invalid.includes(entry)) as EdgePrimitive[],
|
primitives: primitives.filter((entry) => !invalid.includes(entry)) as EdgePrimitive[],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -1218,7 +1254,7 @@ const lowerGenericPackageExport = (
|
|||||||
const parameters = state.types.declareParameters(context.typeParameters(), JSON.stringify(["export", id]));
|
const parameters = state.types.declareParameters(context.typeParameters(), JSON.stringify(["export", id]));
|
||||||
const dependencyPorts = (context.dependencyBlock()?.dependencyPort() ?? []).map((port): GenericDependencyPort => {
|
const dependencyPorts = (context.dependencyBlock()?.dependencyPort() ?? []).map((port): GenericDependencyPort => {
|
||||||
const base = {
|
const base = {
|
||||||
id: capabilityId.dependencyPort(stringValue(port.stringLiteral())),
|
id: capabilityId.dependencyPort(stringValue(port.stringLiteral(0))),
|
||||||
displayName: identifier(port.identifier(0)),
|
displayName: identifier(port.identifier(0)),
|
||||||
};
|
};
|
||||||
const primitives =
|
const primitives =
|
||||||
@@ -1257,6 +1293,7 @@ const lowerGenericPackageExport = (
|
|||||||
kind: "edge",
|
kind: "edge",
|
||||||
target: state.types.targetExpression(port.targetConstraint()!),
|
target: state.types.targetExpression(port.targetConstraint()!),
|
||||||
cardinality: lowerCardinality(port.cardinality()!),
|
cardinality: lowerCardinality(port.cardinality()!),
|
||||||
|
...(port.KEYED() ? { keyType: stringValue(port.stringLiteral(1)) as "string" | "boolean" | "int64" } : {}),
|
||||||
primitives: primitives as EdgePrimitive[],
|
primitives: primitives as EdgePrimitive[],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -2182,10 +2219,25 @@ const lowerConformanceRule = (state: LoweringState, context: ConformanceDeclCont
|
|||||||
} else if (provider.STATE() || provider.EDGE()) {
|
} else if (provider.STATE() || provider.EDGE()) {
|
||||||
const name = identifier(provider.identifier(0));
|
const name = identifier(provider.identifier(0));
|
||||||
const expected = provider.STATE() ? "state" : "edge";
|
const expected = provider.STATE() ? "state" : "edge";
|
||||||
// A rule may traverse another owner's public inverse projection, just as
|
// Concrete attachments are registered before rules, so unused rules can
|
||||||
// a concrete conformance may. Resolve external edges after specialization;
|
// also check public traversal without waiting for an application.
|
||||||
// ordinary ownership validation still permits only public resolve access.
|
if (provider.EDGE() && !privateStorage.has(name)) {
|
||||||
if (provider.EDGE() && !privateStorage.has(name)) continue;
|
const external = requireSymbol(state, state.attachments, name, provider, "attachment");
|
||||||
|
const projection =
|
||||||
|
external?.attachment.kind === "edge"
|
||||||
|
? external.attachment.endpoints.find(
|
||||||
|
(endpoint) => endpoint.displayName === identifier(provider.identifier(1)),
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
if (external && (!projection?.publicTraversal || text(provider.edgePrimitive()) !== "resolve"))
|
||||||
|
loweringIssue(
|
||||||
|
state,
|
||||||
|
provider,
|
||||||
|
"invalid-rule-storage-binding",
|
||||||
|
"External rule storage access requires a public edge resolve projection",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (privateStorage.get(name) !== expected)
|
if (privateStorage.get(name) !== expected)
|
||||||
loweringIssue(
|
loweringIssue(
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export type GenericDependencyPort = Omit<DependencyPort, "requirement"> & {
|
|||||||
kind: "edge";
|
kind: "edge";
|
||||||
target: ObjectTypeExpression;
|
target: ObjectTypeExpression;
|
||||||
cardinality: Extract<DependencyPort["requirement"], { kind: "edge" }>["cardinality"];
|
cardinality: Extract<DependencyPort["requirement"], { kind: "edge" }>["cardinality"];
|
||||||
|
keyType?: "string" | "boolean" | "int64";
|
||||||
primitives: Extract<DependencyPort["requirement"], { kind: "edge" }>["primitives"];
|
primitives: Extract<DependencyPort["requirement"], { kind: "edge" }>["primitives"];
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ export function validateInitialization(workspace: WorkspaceRevision): Capability
|
|||||||
fail(ownerKey(owner), `Initialization input ${field.input} must name an edge projection on the newborn atom`);
|
fail(ownerKey(owner), `Initialization input ${field.input} must name an edge projection on the newborn atom`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (target.keyType) {
|
||||||
|
fail(
|
||||||
|
ownerKey(owner),
|
||||||
|
"Initialize a keyed relationship through its keyed projection; inverse seeds cannot supply a map key",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (endpoint.cardinality === "many" || endpoint.cardinality === "many-unique") {
|
if (endpoint.cardinality === "many" || endpoint.cardinality === "many-unique") {
|
||||||
const fields: Record<string, ValueType> = {
|
const fields: Record<string, ValueType> = {
|
||||||
key: { kind: "scalar", name: "string" },
|
key: { kind: "scalar", name: "string" },
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export type DependencyPortRequirement =
|
|||||||
kind: "edge";
|
kind: "edge";
|
||||||
target: EdgeEndpointConstraint;
|
target: EdgeEndpointConstraint;
|
||||||
cardinality: EdgeCardinality;
|
cardinality: EdgeCardinality;
|
||||||
|
keyType?: "string" | "boolean" | "int64";
|
||||||
primitives: EdgePrimitive[];
|
primitives: EdgePrimitive[];
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -439,7 +439,19 @@ export const edgePrimitiveSignature = (
|
|||||||
case "disconnect":
|
case "disconnect":
|
||||||
return {
|
return {
|
||||||
mode: "call",
|
mode: "call",
|
||||||
inputType: targetType,
|
inputType:
|
||||||
|
primitive === "connect" && projection.endpoint.keyType
|
||||||
|
? {
|
||||||
|
kind: "record",
|
||||||
|
fields: {
|
||||||
|
mapKey: {
|
||||||
|
kind: "scalar",
|
||||||
|
name: projection.endpoint.keyType === "boolean" ? "bool" : projection.endpoint.keyType,
|
||||||
|
},
|
||||||
|
target: targetType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: targetType,
|
||||||
outputType: valueType.unit,
|
outputType: valueType.unit,
|
||||||
};
|
};
|
||||||
case "watch-start":
|
case "watch-start":
|
||||||
@@ -1398,6 +1410,13 @@ const validateBoundDependencies = (
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (projection.target.keyType && requirement.primitives.includes("connect"))
|
||||||
|
issue(
|
||||||
|
issues,
|
||||||
|
"invalid-dependency-binding",
|
||||||
|
path,
|
||||||
|
"Connect through the keyed projection so the caller supplies a map key",
|
||||||
|
);
|
||||||
const traversal = edgeBinding.via
|
const traversal = edgeBinding.via
|
||||||
? validateTraversal(
|
? validateTraversal(
|
||||||
issues,
|
issues,
|
||||||
@@ -1434,13 +1453,14 @@ const validateBoundDependencies = (
|
|||||||
requirement.target,
|
requirement.target,
|
||||||
params.indexes.conformances,
|
params.indexes.conformances,
|
||||||
) ||
|
) ||
|
||||||
requirement.cardinality !== projection.endpoint.cardinality
|
requirement.cardinality !== projection.endpoint.cardinality ||
|
||||||
|
requirement.keyType !== projection.endpoint.keyType
|
||||||
) {
|
) {
|
||||||
issue(
|
issue(
|
||||||
issues,
|
issues,
|
||||||
"invalid-dependency-binding",
|
"invalid-dependency-binding",
|
||||||
`${path}.binding`,
|
`${path}.binding`,
|
||||||
`Edge port target or cardinality does not match projection ${edgeBinding.projectionId}`,
|
`Edge port target, cardinality or key type does not match projection ${edgeBinding.projectionId}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -1822,6 +1842,13 @@ const validateConformances = (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const projection = edgeProjection(attachment.attachment, binding.projectionId);
|
const projection = edgeProjection(attachment.attachment, binding.projectionId);
|
||||||
|
if (projection?.target.keyType && binding.primitive === "connect")
|
||||||
|
issue(
|
||||||
|
issues,
|
||||||
|
"invalid-edge-binding",
|
||||||
|
bindingPath,
|
||||||
|
"Connect through the keyed projection so the caller supplies a map key",
|
||||||
|
);
|
||||||
// An owned endpoint may explicitly export read-only traversal, including
|
// An owned endpoint may explicitly export read-only traversal, including
|
||||||
// a native inverse relationship view. This never exports mutation rights.
|
// a native inverse relationship view. This never exports mutation rights.
|
||||||
if (!(binding.primitive === "resolve" && projection?.endpoint.publicTraversal)) {
|
if (!(binding.primitive === "resolve" && projection?.endpoint.publicTraversal)) {
|
||||||
|
|||||||
@@ -53,11 +53,11 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const endpoints = storage.endpoints.map((endpoint) => {
|
const endpoints = storage.endpoints.map((endpoint) => {
|
||||||
if (endpoint.ordered || endpoint.keyType || endpoint.retainOther)
|
if (endpoint.ordered || (endpoint.keyType && endpoint.keyType !== "string") || endpoint.retainOther)
|
||||||
throw new ExecutionPlanError(
|
throw new ExecutionPlanError(
|
||||||
"UNSUPPORTED_GRAPH_FEATURE",
|
"UNSUPPORTED_GRAPH_FEATURE",
|
||||||
storage.id,
|
storage.id,
|
||||||
"Ordered/keyed relations and automatic retention are not implemented",
|
"Ordered relations, non-string keys and automatic retention are not implemented",
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
projection: endpoint.projectionId,
|
projection: endpoint.projectionId,
|
||||||
@@ -69,6 +69,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
|||||||
: endpoint.constraint.interfaceRevisionId,
|
: endpoint.constraint.interfaceRevisionId,
|
||||||
},
|
},
|
||||||
cardinality: endpoint.cardinality,
|
cardinality: endpoint.cardinality,
|
||||||
|
...(endpoint.keyType ? { keyType: endpoint.keyType } : {}),
|
||||||
...(endpoint.onDelete ? { onDelete: endpoint.onDelete } : {}),
|
...(endpoint.onDelete ? { onDelete: endpoint.onDelete } : {}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,11 +10,7 @@ export function unsupportedQueryOperator(query: CheckedQuery): string | undefine
|
|||||||
if (value.kind === "reduce") return "aggregate predicates";
|
if (value.kind === "reduce") return "aggregate predicates";
|
||||||
if (value.kind === "and" || value.kind === "or") return value.children.map(predicate).find(Boolean);
|
if (value.kind === "and" || value.kind === "or") return value.children.map(predicate).find(Boolean);
|
||||||
if (value.kind === "not") return predicate(value.child);
|
if (value.kind === "not") return predicate(value.child);
|
||||||
if (value.kind === "compare")
|
if (value.kind === "compare") return path(value.expression.path);
|
||||||
return (
|
|
||||||
path(value.expression.path) ??
|
|
||||||
(value.expression.leaf.kind === "mapKey" ? "keyed relationship operands" : undefined)
|
|
||||||
);
|
|
||||||
if (value.kind === "relation") return path(value.path) ?? (value.predicate && predicate(value.predicate));
|
if (value.kind === "relation") return path(value.path) ?? (value.predicate && predicate(value.predicate));
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -230,7 +230,13 @@ export async function compileQuery(
|
|||||||
fail("QUERY_UNSUPPORTED_FEATURE", "Bounded-all does not accept a continuation", node);
|
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")
|
if (argument.name.value === "where")
|
||||||
compiledPredicates.set(node, relational.predicate(objectRow(target), argument.value));
|
compiledPredicates.set(
|
||||||
|
node,
|
||||||
|
relational.predicate(
|
||||||
|
objectRow(target, member.keyType ? { keyType: member.keyType } : undefined),
|
||||||
|
argument.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
if (argument.name.value === "orderBy") inputEffects(target, argument.value, "order");
|
if (argument.name.value === "orderBy") inputEffects(target, argument.value, "order");
|
||||||
if (
|
if (
|
||||||
["first", "all"].includes(argument.name.value) &&
|
["first", "all"].includes(argument.name.value) &&
|
||||||
|
|||||||
+5
-1
@@ -242,7 +242,11 @@ export function querySchema(
|
|||||||
first: { type: GraphQLInt },
|
first: { type: GraphQLInt },
|
||||||
all: { type: GraphQLInt },
|
all: { type: GraphQLInt },
|
||||||
after: { type: cursorScalar },
|
after: { type: cursorScalar },
|
||||||
where: { type: filter(targetId) },
|
where: {
|
||||||
|
type: member.keyType
|
||||||
|
? relational.where(objectRow(targetId, { keyType: member.keyType }))
|
||||||
|
: filter(targetId),
|
||||||
|
},
|
||||||
...(ordering ? { orderBy: { type: new GraphQLList(new GraphQLNonNull(ordering)) } } : {}),
|
...(ordering ? { orderBy: { type: new GraphQLList(new GraphQLNonNull(ordering)) } } : {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -344,4 +344,11 @@ test("rule inverse reads retain the concrete conformance ownership checks", () =
|
|||||||
const missing = compile(source.replace("Membership.parent.resolve", "Missing.parent.resolve"));
|
const missing = compile(source.replace("Membership.parent.resolve", "Missing.parent.resolve"));
|
||||||
assert.equal(missing.ok, false);
|
assert.equal(missing.ok, false);
|
||||||
assert.match(JSON.stringify(missing.diagnostics), /Unknown attachment/);
|
assert.match(JSON.stringify(missing.diagnostics), /Unknown attachment/);
|
||||||
|
const unused = compile(
|
||||||
|
source
|
||||||
|
.replace('conform C as Child id "child-impl" {}', "")
|
||||||
|
.replace("Membership.parent.resolve", "Missing.parent.resolve"),
|
||||||
|
);
|
||||||
|
assert.equal(unused.ok, false);
|
||||||
|
assert.match(JSON.stringify(unused.diagnostics), /Unknown attachment/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -129,6 +129,10 @@ await writeFile("dist/portable-reader/collections/kernel.json", JSON.stringify(c
|
|||||||
const { presentationMountPlan } = await import("./presentation-mount-fixture.js");
|
const { presentationMountPlan } = await import("./presentation-mount-fixture.js");
|
||||||
const presentationMount = await presentationMountPlan();
|
const presentationMount = await presentationMountPlan();
|
||||||
await mkdir("dist/portable-reader/presentation-mount", { recursive: true });
|
await mkdir("dist/portable-reader/presentation-mount", { recursive: true });
|
||||||
|
await writeFile(
|
||||||
|
"dist/portable-reader/presentation-mount/queries.json",
|
||||||
|
JSON.stringify(compileQueryProgram(presentationMount)),
|
||||||
|
);
|
||||||
await writeFile("dist/portable-reader/presentation-mount/plan.json", JSON.stringify(presentationMount));
|
await writeFile("dist/portable-reader/presentation-mount/plan.json", JSON.stringify(presentationMount));
|
||||||
await writeFile(
|
await writeFile(
|
||||||
"dist/portable-reader/presentation-mount/kernel.json",
|
"dist/portable-reader/presentation-mount/kernel.json",
|
||||||
|
|||||||
+8
-1
@@ -1,3 +1,6 @@
|
|||||||
|
import interface PresentationStates;
|
||||||
|
import interface ReaderNavigation;
|
||||||
|
external atom ReaderNavigationRecord id "reader-navigation-record";
|
||||||
import interface PresentationState;
|
import interface PresentationState;
|
||||||
import interface PresentationMount;
|
import interface PresentationMount;
|
||||||
import interface StateFactory;
|
import interface StateFactory;
|
||||||
@@ -12,7 +15,7 @@ package PresentationRuntime id "presentation-runtime" revision "presentation-run
|
|||||||
interface metadata id "state" : PresentationState using [presentationMount.resolve,parent.resolve] from input;
|
interface metadata id "state" : PresentationState using [presentationMount.resolve,parent.resolve] from input;
|
||||||
};
|
};
|
||||||
operation ensure<object Navigation implements PresentationState> id "presentation-runtime:ensure" : record {presentationMount:interface-ref<PresentationMount>;occurrence:string;parent:optional<interface-ref<PresentationState>>;} -> ref<Navigation> mode call execution transaction targets [browser,server] receiver any requires {
|
operation ensure<object Navigation implements PresentationState> id "presentation-runtime:ensure" : record {presentationMount:interface-ref<PresentationMount>;occurrence:string;parent:optional<interface-ref<PresentationState>>;} -> ref<Navigation> mode call execution transaction targets [browser,server] receiver any requires {
|
||||||
edge states id "states" : many-unique object Navigation [resolve,connect];
|
edge states id "states" : many-unique object Navigation keyed "string" [resolve,connect];
|
||||||
interface metadata id "state" : PresentationState using [occurrence.get,presentationMount.resolve,parent.resolve] from input;
|
interface metadata id "state" : PresentationState using [occurrence.get,presentationMount.resolve,parent.resolve] from input;
|
||||||
interface presentationMount id "presentationMount" : PresentationMount using [attach.call] from input;
|
interface presentationMount id "presentationMount" : PresentationMount using [attach.call] from input;
|
||||||
interface factory id "factory" : StateFactory<Navigation> using [create.call];
|
interface factory id "factory" : StateFactory<Navigation> using [create.call];
|
||||||
@@ -20,4 +23,8 @@ package PresentationRuntime id "presentation-runtime" revision "presentation-run
|
|||||||
function createState<object Navigation> id "presentation-runtime:create-state" : record {occurrence:string;parent:optional<interface-ref<PresentationState>>;} -> ref<Navigation> execution transaction targets [browser,server] requires {
|
function createState<object Navigation> id "presentation-runtime:create-state" : record {occurrence:string;parent:optional<interface-ref<PresentationState>>;} -> ref<Navigation> execution transaction targets [browser,server] requires {
|
||||||
creation navigation id "navigation" : Navigation input record {occurrence:string;parent:optional<interface-ref<PresentationState>>;};
|
creation navigation id "navigation" : Navigation input record {occurrence:string;parent:optional<interface-ref<PresentationState>>;};
|
||||||
};
|
};
|
||||||
|
query ReaderStateInputs id "reader-state:inputs" root PresentationStates<atom ReaderNavigationRecord> document "queries/reader-inputs.graphql" operation "ReaderState" { replica; view ReaderNavigationRecord as ReaderNavigation; max rows 100; }
|
||||||
|
query ReaderStateView id "reader-state:view" root PresentationStates<atom ReaderNavigationRecord> document "queries/reader-state.graphql" operation "ReaderState" { local from ReaderStateInputs; watch; view ReaderNavigationRecord as ReaderNavigation; max rows 100; }
|
||||||
|
query ReaderWindowInputs id "reader-state:window-inputs" root PresentationStates<atom ReaderNavigationRecord> document "queries/reader-window.graphql" operation "ReaderState" { replica; view ReaderNavigationRecord as ReaderNavigation; max rows 100; }
|
||||||
|
query ReaderWindowView id "reader-state:window-view" root PresentationStates<atom ReaderNavigationRecord> document "queries/reader-window.graphql" operation "ReaderState" { local from ReaderWindowInputs; watch; view ReaderNavigationRecord as ReaderNavigation; max rows 100; }
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import interface PresentationState;
|
import interface PresentationState;
|
||||||
import interface PresentationMount;
|
import interface PresentationMount;
|
||||||
interface PresentationStates<object Navigation implements PresentationState> id "presentation-states" revision "presentation-states@1" {
|
interface PresentationStates<object Navigation implements PresentationState> id "presentation-states" revision "presentation-states@1" {
|
||||||
queryable relation states id "presentation-states:states" : many-unique object Navigation {
|
queryable relation states id "presentation-states:states" : many-unique object Navigation keyed "string" {
|
||||||
resolve id "presentation-states:states:resolve" execution snapshot targets [browser,server];
|
resolve id "presentation-states:states:resolve" execution snapshot targets [browser,server];
|
||||||
}
|
}
|
||||||
operation ensure id "presentation-states:ensure" : record {presentationMount:interface-ref<PresentationMount>;occurrence:string;parent:optional<interface-ref<PresentationState>>;} -> ref<Navigation> {
|
operation ensure id "presentation-states:ensure" : record {presentationMount:interface-ref<PresentationMount>;occurrence:string;parent:optional<interface-ref<PresentationState>>;} -> ref<Navigation> {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
query ReaderState {
|
||||||
|
root {
|
||||||
|
states(all: 100) {
|
||||||
|
entries { key mapKey node { _camino { ref } page } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
query ReaderState($key: String!) {
|
||||||
|
root {
|
||||||
|
states(all: 100, where: {_camino: {mapKey: {eq: $key}}}) {
|
||||||
|
entries { key mapKey node { _camino { ref } page } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
query ReaderState($key: String!) {
|
||||||
|
root {
|
||||||
|
states(first: 100, where: {_camino: {mapKey: {eq: $key}}}) {
|
||||||
|
entries { key mapKey node { _camino { ref } page } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-2
@@ -10,6 +10,7 @@ workspace Example id "presentation-example" revision "presentation-example@1" co
|
|||||||
import package PresentationRuntime;
|
import package PresentationRuntime;
|
||||||
atom PresentationMountRecord id "presentation-mount-record";
|
atom PresentationMountRecord id "presentation-mount-record";
|
||||||
atom Library id "library";
|
atom Library id "library";
|
||||||
|
atom OtherLibrary id "other-library";
|
||||||
atom Document id "document";
|
atom Document id "document";
|
||||||
atom ReaderNavigationRecord id "reader-navigation-record";
|
atom ReaderNavigationRecord id "reader-navigation-record";
|
||||||
atom DocumentNavigationRecord id "document-navigation-record";
|
atom DocumentNavigationRecord id "document-navigation-record";
|
||||||
@@ -34,8 +35,8 @@ workspace Example id "presentation-example" revision "presentation-example@1" co
|
|||||||
}
|
}
|
||||||
conform<object Navigation implements PresentationState> Presentation<Navigation> as PresentationStates<Navigation> id "presentation:state-association" {
|
conform<object Navigation implements PresentationState> Presentation<Navigation> as PresentationStates<Navigation> id "presentation:state-association" {
|
||||||
private edge States id "presentation:states-edge" {
|
private edge States id "presentation:states-edge" {
|
||||||
atom Self projection states id "presentation:states-projection" many-unique;
|
atom Self projection states id "presentation:states-projection" many-unique keyed "string";
|
||||||
object Navigation projection subject id "presentation:subject-projection" exactly-one;
|
object Navigation projection subject id "presentation:subject-projection" optional-one;
|
||||||
}
|
}
|
||||||
bind states.resolve to edge States.states.resolve;
|
bind states.resolve to edge States.states.resolve;
|
||||||
bind ensure.call to package PresentationRuntime.ensure<Navigation> with {
|
bind ensure.call to package PresentationRuntime.ensure<Navigation> with {
|
||||||
@@ -78,4 +79,10 @@ workspace Example id "presentation-example" revision "presentation-example@1" co
|
|||||||
}
|
}
|
||||||
conform Library as Presentation<atom ReaderNavigationRecord> id "library:presentation" {}
|
conform Library as Presentation<atom ReaderNavigationRecord> id "library:presentation" {}
|
||||||
conform Document as Presentation<atom DocumentNavigationRecord> id "document:presentation" {}
|
conform Document as Presentation<atom DocumentNavigationRecord> id "document:presentation" {}
|
||||||
|
conform OtherLibrary as StateFactory<atom ReaderNavigationRecord> id "other-reader:factory" {
|
||||||
|
bind create.call to package PresentationRuntime.createState<atom ReaderNavigationRecord> with {
|
||||||
|
navigation to creation ReaderNavigationRecord with {PresentationState from input;};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
conform OtherLibrary as Presentation<atom ReaderNavigationRecord> id "other-library:presentation" {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import {
|
||||||
|
compileCapabilityResourceSource,
|
||||||
|
compileCapabilitySource,
|
||||||
|
formatQx,
|
||||||
|
} from "../src/capability-language/index.js";
|
||||||
|
import { compileExecutionPlan } from "../src/execution-world/plan.js";
|
||||||
|
import { compileKernelProgram } from "../src/execution-world/kernel.js";
|
||||||
|
import { generatePortableTypeScriptBindings } from "../src/bindings/portable.js";
|
||||||
|
const source = { repository: "https://example.test/keyed.git", commit: "a".repeat(40) };
|
||||||
|
const iface = compileCapabilityResourceSource(
|
||||||
|
`interface Index<object Item> id "index" revision "index@1" {
|
||||||
|
queryable relation entries id "entries" : many object Item keyed "string" {
|
||||||
|
resolve id "resolve" execution snapshot targets [browser,server];
|
||||||
|
connect id "connect" execution transaction targets [browser,server];
|
||||||
|
}
|
||||||
|
operation add id "add" : record {mapKey:string;target:ref<Item>;} -> unit {
|
||||||
|
call id "add-call" execution transaction targets [browser,server];
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{ source },
|
||||||
|
);
|
||||||
|
assert.ok(iface.ok && iface.resource.kind === "interface", JSON.stringify(iface.diagnostics));
|
||||||
|
const pkg = compileCapabilityResourceSource(
|
||||||
|
`package Runtime id "runtime" revision "runtime@1" {
|
||||||
|
operation add<object Item> id "add" : record {mapKey:string;target:ref<Item>;} -> unit mode call execution transaction targets [browser,server] receiver any requires {
|
||||||
|
edge entries id "entries" : many object Item keyed "string" [resolve,connect];
|
||||||
|
};
|
||||||
|
}`,
|
||||||
|
{ source },
|
||||||
|
);
|
||||||
|
assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics));
|
||||||
|
const indexContract = iface.resource.revision,
|
||||||
|
runtimeContract = pkg.resource.revision;
|
||||||
|
const declarations = `workspace W id "w" revision "w@1" commit "${source.commit}" {
|
||||||
|
import interface Index; import package Runtime; atom Node id "node";
|
||||||
|
conform Node as Index<atom Node> id "node:index" {
|
||||||
|
private edge Entries id "entries" {
|
||||||
|
atom Node projection entries id "entries" many keyed "string";
|
||||||
|
atom Node projection parent id "parent" optional-one;
|
||||||
|
}
|
||||||
|
bind entries.resolve to edge Entries.entries.resolve;
|
||||||
|
bind entries.connect to edge Entries.entries.connect;
|
||||||
|
bind add.call to package Runtime.add<atom Node> with {entries to edge Entries.entries;};
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
const compile = (text = declarations) =>
|
||||||
|
compileCapabilitySource(formatQx(text), "workspace.qx", {
|
||||||
|
interfaces: new Map([["Index", indexContract]]),
|
||||||
|
packages: new Map([["Runtime", runtimeContract]]),
|
||||||
|
});
|
||||||
|
test("generic keyed relationships and portable ports have matching checked shapes", () => {
|
||||||
|
const result = compile();
|
||||||
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||||
|
const closed = result.workspace.interfaceImports.find((i) => i.application)!;
|
||||||
|
assert.equal(closed.members[0]!.kind, "relationship");
|
||||||
|
assert.equal("keyType" in closed.members[0]! && closed.members[0]!.keyType, "string");
|
||||||
|
const operation = closed.members[0]!.operations.find((o) => o.displayName === "connect")!;
|
||||||
|
assert.equal(operation.inputType.kind, "record");
|
||||||
|
const plan = compileExecutionPlan(result.workspace);
|
||||||
|
assert.doesNotThrow(() => compileKernelProgram(plan));
|
||||||
|
const bindings = generatePortableTypeScriptBindings(plan.definition.bindingSchema, "runtime@1", {
|
||||||
|
target: "browser",
|
||||||
|
});
|
||||||
|
assert.match(bindings, /readonly "mapKey": string/);
|
||||||
|
assert.match(bindings, /readonly "connect": \(input: \{ readonly "mapKey": string;/);
|
||||||
|
});
|
||||||
|
test("key contracts cannot silently bind to unkeyed storage or the inverse projection", () => {
|
||||||
|
for (const text of [
|
||||||
|
declarations.replace('many keyed "string";', "many;"),
|
||||||
|
declarations.replace("entries to edge Entries.entries", "entries to edge Entries.parent"),
|
||||||
|
]) {
|
||||||
|
const result = compile(text);
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.match(JSON.stringify(result.diagnostics), /key type|keyed projection|signature/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
|
|||||||
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
|
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
|
||||||
import { compileExecutionPlan } from "../src/execution-world/plan.js";
|
import { compileExecutionPlan } from "../src/execution-world/plan.js";
|
||||||
import type { InterfaceRevision } from "../src/capability-model/types.js";
|
import type { InterfaceRevision } from "../src/capability-model/types.js";
|
||||||
|
import { compileQuery } from "../src/query/compile.js";
|
||||||
export async function presentationMountPlan() {
|
export async function presentationMountPlan() {
|
||||||
const read = (name: string) => readFile(`test/fixtures/presentation-mount/${name}.qx`, "utf8");
|
const read = (name: string) => readFile(`test/fixtures/presentation-mount/${name}.qx`, "utf8");
|
||||||
const source = { repository: "https://example.test/presentation-mount.git", commit: "a".repeat(40) };
|
const source = { repository: "https://example.test/presentation-mount.git", commit: "a".repeat(40) };
|
||||||
@@ -31,5 +32,13 @@ export async function presentationMountPlan() {
|
|||||||
packages: new Map([["PresentationRuntime", pkg.resource.revision]]),
|
packages: new Map([["PresentationRuntime", pkg.resource.revision]]),
|
||||||
});
|
});
|
||||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||||
|
const runtime = result.workspace.packageImports[0]!;
|
||||||
|
runtime.checkedQueries = await Promise.all(
|
||||||
|
runtime.queries!.map((query) =>
|
||||||
|
compileQuery(query, result.workspace.interfaceImports, (path) =>
|
||||||
|
readFile(`test/fixtures/presentation-mount/${path}`, "utf8"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
return compileExecutionPlan(result.workspace);
|
return compileExecutionPlan(result.workspace);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user