Support checked keyed relationships across portable operations and queries
This commit is contained in:
@@ -163,12 +163,16 @@ function methods(
|
||||
});
|
||||
case "edge": {
|
||||
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) => {
|
||||
if (primitive === "resolve")
|
||||
return method("resolve", { kind: "edge", primitive }, unit, { kind: "list", value: item });
|
||||
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);
|
||||
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"],
|
||||
target: EdgeEndpointConstraint,
|
||||
cardinality: EdgeCardinality,
|
||||
keyType?: "string" | "boolean" | "int64",
|
||||
): InterfaceOperation => {
|
||||
const targetType =
|
||||
target.kind === "atom" ? valueType.atomRef(target.atomId) : valueType.interfaceRef(target.interfaceRevisionId);
|
||||
@@ -463,7 +464,16 @@ const relationshipOperation = (
|
||||
return {
|
||||
id,
|
||||
displayName,
|
||||
inputType: targetType,
|
||||
inputType:
|
||||
keyType && displayName === "connect"
|
||||
? {
|
||||
kind: "record",
|
||||
fields: {
|
||||
mapKey: { kind: "scalar", name: keyType === "boolean" ? "bool" : keyType },
|
||||
target: targetType,
|
||||
},
|
||||
}
|
||||
: targetType,
|
||||
outputType: valueType.unit,
|
||||
mode: "call",
|
||||
};
|
||||
@@ -545,6 +555,9 @@ const lowerRelationshipMember = (
|
||||
return undefined;
|
||||
}
|
||||
const cardinality = lowerCardinality(context.cardinality());
|
||||
const keyType = context.KEYED()
|
||||
? (stringValue(context.stringLiteral(1)) as "string" | "boolean" | "int64")
|
||||
: undefined;
|
||||
const operations: InterfaceOperation[] = [];
|
||||
for (const operation of context.relationshipOperation()) {
|
||||
if (operation.RESOLVE()) {
|
||||
@@ -554,6 +567,7 @@ const lowerRelationshipMember = (
|
||||
capabilityId.operation(stringValue(operation.stringLiteral(0))),
|
||||
target,
|
||||
cardinality,
|
||||
keyType,
|
||||
),
|
||||
);
|
||||
} else if (operation.CONNECT()) {
|
||||
@@ -563,6 +577,7 @@ const lowerRelationshipMember = (
|
||||
capabilityId.operation(stringValue(operation.stringLiteral(0))),
|
||||
target,
|
||||
cardinality,
|
||||
keyType,
|
||||
),
|
||||
);
|
||||
} else if (operation.DISCONNECT()) {
|
||||
@@ -572,6 +587,7 @@ const lowerRelationshipMember = (
|
||||
capabilityId.operation(stringValue(operation.stringLiteral(0))),
|
||||
target,
|
||||
cardinality,
|
||||
keyType,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
@@ -581,12 +597,14 @@ const lowerRelationshipMember = (
|
||||
capabilityId.operation(stringValue(operation.stringLiteral(0))),
|
||||
target,
|
||||
cardinality,
|
||||
keyType,
|
||||
),
|
||||
relationshipOperation(
|
||||
"watch-stop",
|
||||
capabilityId.operation(stringValue(operation.stringLiteral(1))),
|
||||
target,
|
||||
cardinality,
|
||||
keyType,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -847,6 +865,15 @@ const lowerInterfaceTemplate = (
|
||||
const target = types.targetExpression(relationship.targetConstraint());
|
||||
const targetType: ValueTypeExpression = { kind: "object-ref", expectation: target };
|
||||
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 =
|
||||
cardinality === "exactly-one"
|
||||
? targetType
|
||||
@@ -872,6 +899,7 @@ const lowerInterfaceTemplate = (
|
||||
target,
|
||||
cardinality,
|
||||
ordered: Boolean(relationship.ORDERED()),
|
||||
...(keyType ? { keyType } : {}),
|
||||
operations: relationship.relationshipOperation().flatMap((operation) => {
|
||||
const id = capabilityId.operation(stringValue(operation.stringLiteral(0)));
|
||||
if (operation.RESOLVE())
|
||||
@@ -881,7 +909,12 @@ const lowerInterfaceTemplate = (
|
||||
if (operation.CONNECT() || operation.DISCONNECT())
|
||||
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()),
|
||||
},
|
||||
];
|
||||
@@ -957,7 +990,7 @@ const lowerQueryReference = (
|
||||
|
||||
const lowerDependencyPort = (state: LoweringState, context: DependencyPortContext): DependencyPort | undefined => {
|
||||
const name = identifier(context.identifier(0));
|
||||
const id = capabilityId.dependencyPort(stringValue(context.stringLiteral()));
|
||||
const id = capabilityId.dependencyPort(stringValue(context.stringLiteral(0)));
|
||||
if (context.REFLECTION())
|
||||
return {
|
||||
id,
|
||||
@@ -1016,6 +1049,9 @@ const lowerDependencyPort = (state: LoweringState, context: DependencyPortContex
|
||||
kind: "edge",
|
||||
target,
|
||||
cardinality: lowerCardinality(context.cardinality()),
|
||||
...(context.KEYED()
|
||||
? { keyType: stringValue(context.stringLiteral(1)) as "string" | "boolean" | "int64" }
|
||||
: {}),
|
||||
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 dependencyPorts = (context.dependencyBlock()?.dependencyPort() ?? []).map((port): GenericDependencyPort => {
|
||||
const base = {
|
||||
id: capabilityId.dependencyPort(stringValue(port.stringLiteral())),
|
||||
id: capabilityId.dependencyPort(stringValue(port.stringLiteral(0))),
|
||||
displayName: identifier(port.identifier(0)),
|
||||
};
|
||||
const primitives =
|
||||
@@ -1257,6 +1293,7 @@ const lowerGenericPackageExport = (
|
||||
kind: "edge",
|
||||
target: state.types.targetExpression(port.targetConstraint()!),
|
||||
cardinality: lowerCardinality(port.cardinality()!),
|
||||
...(port.KEYED() ? { keyType: stringValue(port.stringLiteral(1)) as "string" | "boolean" | "int64" } : {}),
|
||||
primitives: primitives as EdgePrimitive[],
|
||||
},
|
||||
};
|
||||
@@ -2182,10 +2219,25 @@ const lowerConformanceRule = (state: LoweringState, context: ConformanceDeclCont
|
||||
} else if (provider.STATE() || provider.EDGE()) {
|
||||
const name = identifier(provider.identifier(0));
|
||||
const expected = provider.STATE() ? "state" : "edge";
|
||||
// A rule may traverse another owner's public inverse projection, just as
|
||||
// a concrete conformance may. Resolve external edges after specialization;
|
||||
// ordinary ownership validation still permits only public resolve access.
|
||||
if (provider.EDGE() && !privateStorage.has(name)) continue;
|
||||
// Concrete attachments are registered before rules, so unused rules can
|
||||
// also check public traversal without waiting for an application.
|
||||
if (provider.EDGE() && !privateStorage.has(name)) {
|
||||
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)
|
||||
loweringIssue(
|
||||
state,
|
||||
|
||||
@@ -32,6 +32,7 @@ export type GenericDependencyPort = Omit<DependencyPort, "requirement"> & {
|
||||
kind: "edge";
|
||||
target: ObjectTypeExpression;
|
||||
cardinality: Extract<DependencyPort["requirement"], { kind: "edge" }>["cardinality"];
|
||||
keyType?: "string" | "boolean" | "int64";
|
||||
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`);
|
||||
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") {
|
||||
const fields: Record<string, ValueType> = {
|
||||
key: { kind: "scalar", name: "string" },
|
||||
|
||||
@@ -250,6 +250,7 @@ export type DependencyPortRequirement =
|
||||
kind: "edge";
|
||||
target: EdgeEndpointConstraint;
|
||||
cardinality: EdgeCardinality;
|
||||
keyType?: "string" | "boolean" | "int64";
|
||||
primitives: EdgePrimitive[];
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -439,7 +439,19 @@ export const edgePrimitiveSignature = (
|
||||
case "disconnect":
|
||||
return {
|
||||
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,
|
||||
};
|
||||
case "watch-start":
|
||||
@@ -1398,6 +1410,13 @@ const validateBoundDependencies = (
|
||||
);
|
||||
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
|
||||
? validateTraversal(
|
||||
issues,
|
||||
@@ -1434,13 +1453,14 @@ const validateBoundDependencies = (
|
||||
requirement.target,
|
||||
params.indexes.conformances,
|
||||
) ||
|
||||
requirement.cardinality !== projection.endpoint.cardinality
|
||||
requirement.cardinality !== projection.endpoint.cardinality ||
|
||||
requirement.keyType !== projection.endpoint.keyType
|
||||
) {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-dependency-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;
|
||||
@@ -1822,6 +1842,13 @@ const validateConformances = (
|
||||
continue;
|
||||
}
|
||||
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
|
||||
// a native inverse relationship view. This never exports mutation rights.
|
||||
if (!(binding.primitive === "resolve" && projection?.endpoint.publicTraversal)) {
|
||||
|
||||
@@ -53,11 +53,11 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
||||
});
|
||||
} else {
|
||||
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(
|
||||
"UNSUPPORTED_GRAPH_FEATURE",
|
||||
storage.id,
|
||||
"Ordered/keyed relations and automatic retention are not implemented",
|
||||
"Ordered relations, non-string keys and automatic retention are not implemented",
|
||||
);
|
||||
return {
|
||||
projection: endpoint.projectionId,
|
||||
@@ -69,6 +69,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
||||
: endpoint.constraint.interfaceRevisionId,
|
||||
},
|
||||
cardinality: endpoint.cardinality,
|
||||
...(endpoint.keyType ? { keyType: endpoint.keyType } : {}),
|
||||
...(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 === "and" || value.kind === "or") return value.children.map(predicate).find(Boolean);
|
||||
if (value.kind === "not") return predicate(value.child);
|
||||
if (value.kind === "compare")
|
||||
return (
|
||||
path(value.expression.path) ??
|
||||
(value.expression.leaf.kind === "mapKey" ? "keyed relationship operands" : undefined)
|
||||
);
|
||||
if (value.kind === "compare") return path(value.expression.path);
|
||||
if (value.kind === "relation") return path(value.path) ?? (value.predicate && predicate(value.predicate));
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -230,7 +230,13 @@ export async function compileQuery(
|
||||
fail("QUERY_UNSUPPORTED_FEATURE", "Bounded-all does not accept a continuation", node);
|
||||
for (const argument of node.arguments ?? []) {
|
||||
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 (
|
||||
["first", "all"].includes(argument.name.value) &&
|
||||
|
||||
+5
-1
@@ -242,7 +242,11 @@ export function querySchema(
|
||||
first: { type: GraphQLInt },
|
||||
all: { type: GraphQLInt },
|
||||
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)) } } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user