Introduce persisted Web Studio component capabilities
This commit is contained in:
@@ -51,6 +51,7 @@ export type CapabilityValidationIssueCode =
|
||||
| "duplicate-dependency-port"
|
||||
| "duplicate-conformance"
|
||||
| "duplicate-operation-binding"
|
||||
| "duplicate-relationship-materialization"
|
||||
| "duplicate-constructor"
|
||||
| "unresolved-reference"
|
||||
| "invalid-attachment"
|
||||
@@ -64,7 +65,8 @@ export type CapabilityValidationIssueCode =
|
||||
| "cyclic-conformance-requirement"
|
||||
| "missing-operation-binding"
|
||||
| "unknown-operation-binding"
|
||||
| "invalid-constructor";
|
||||
| "invalid-constructor"
|
||||
| "invalid-relationship-materialization";
|
||||
|
||||
export interface CapabilityValidationIssue {
|
||||
code: CapabilityValidationIssueCode;
|
||||
@@ -460,6 +462,16 @@ const atomSatisfiesConstraint = (
|
||||
? constraint.atomId === atomId
|
||||
: conformances.has(conformanceKey(atomId, constraint.interfaceRevisionId));
|
||||
|
||||
const constraintSatisfiesConstraint = (
|
||||
actual: EdgeEndpointConstraint,
|
||||
required: EdgeEndpointConstraint,
|
||||
conformances: ReadonlyMap<string, Conformance>,
|
||||
) => constraintsEqual(actual, required) || (
|
||||
actual.kind === "atom" &&
|
||||
required.kind === "interface" &&
|
||||
atomSatisfiesConstraint(actual.atomId, required, conformances)
|
||||
);
|
||||
|
||||
const canAccessAttachment = (
|
||||
owner: AttachmentOwner,
|
||||
conformance: Pick<Conformance, "atomId" | "interfaceRevisionId"> | undefined,
|
||||
@@ -958,6 +970,52 @@ const validateAttachmentAccess = (
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateTraversal = (
|
||||
issues: CapabilityValidationIssue[],
|
||||
indexes: ValidationIndexes,
|
||||
atomId: AtomId,
|
||||
traversal: { edgeTypeId: EdgeTypeId; projectionId: EdgeProjectionId },
|
||||
path: string,
|
||||
) => {
|
||||
const attachment = findAttachment(indexes, "edge", traversal.edgeTypeId);
|
||||
if (!attachment || attachment.attachment.kind !== "edge") {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-dependency-binding",
|
||||
`${path}.edgeTypeId`,
|
||||
`Unknown traversal edge ${traversal.edgeTypeId}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
const projection = edgeProjection(attachment.attachment, traversal.projectionId);
|
||||
if (!projection) {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-dependency-binding",
|
||||
`${path}.projectionId`,
|
||||
`Traversal projection ${traversal.projectionId} is not on edge ${traversal.edgeTypeId}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
if (!atomSatisfiesConstraint(atomId, projection.endpoint.constraint, indexes.conformances)) {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-dependency-binding",
|
||||
`${path}.projectionId`,
|
||||
`Traversal projection ${traversal.projectionId} cannot originate at receiver atom ${atomId}`,
|
||||
);
|
||||
}
|
||||
if (projection.endpoint.cardinality !== "exactly-one") {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-dependency-binding",
|
||||
`${path}.projectionId`,
|
||||
"Dependency traversal must select exactly one object",
|
||||
);
|
||||
}
|
||||
return projection;
|
||||
};
|
||||
|
||||
const validateBoundDependencies = (
|
||||
issues: CapabilityValidationIssue[],
|
||||
params: {
|
||||
@@ -1020,12 +1078,31 @@ const validateBoundDependencies = (
|
||||
break;
|
||||
}
|
||||
validateAttachmentAccess(issues, attachment, params.conformance, `${path}.binding.slotId`);
|
||||
if (attachment.attachment.attachedTo !== params.atomId) {
|
||||
const traversal = stateBinding.via
|
||||
? validateTraversal(
|
||||
issues,
|
||||
params.indexes,
|
||||
params.atomId,
|
||||
stateBinding.via,
|
||||
`${path}.binding.via`,
|
||||
)
|
||||
: undefined;
|
||||
if (
|
||||
stateBinding.via
|
||||
? traversal && !atomSatisfiesConstraint(
|
||||
attachment.attachment.attachedTo,
|
||||
traversal.target.constraint,
|
||||
params.indexes.conformances,
|
||||
)
|
||||
: attachment.attachment.attachedTo !== params.atomId
|
||||
) {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-dependency-binding",
|
||||
`${path}.binding.slotId`,
|
||||
`State ${stateBinding.slotId} is not attached to receiver atom ${params.atomId}`,
|
||||
stateBinding.via
|
||||
? `State ${stateBinding.slotId} is not attached to the traversal target`
|
||||
: `State ${stateBinding.slotId} is not attached to receiver atom ${params.atomId}`,
|
||||
);
|
||||
}
|
||||
if (!valueTypesEqual(requirement.valueType, attachment.attachment.valueType)) {
|
||||
@@ -1061,16 +1138,46 @@ const validateBoundDependencies = (
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (!atomSatisfiesConstraint(params.atomId, projection.endpoint.constraint, params.indexes.conformances)) {
|
||||
const traversal = edgeBinding.via
|
||||
? validateTraversal(
|
||||
issues,
|
||||
params.indexes,
|
||||
params.atomId,
|
||||
edgeBinding.via,
|
||||
`${path}.binding.via`,
|
||||
)
|
||||
: undefined;
|
||||
const originMatches = edgeBinding.via
|
||||
? traversal && (
|
||||
projection.endpoint.constraint.kind === "atom"
|
||||
? atomSatisfiesConstraint(
|
||||
projection.endpoint.constraint.atomId,
|
||||
traversal.target.constraint,
|
||||
params.indexes.conformances,
|
||||
)
|
||||
: constraintsEqual(projection.endpoint.constraint, traversal.target.constraint)
|
||||
)
|
||||
: atomSatisfiesConstraint(
|
||||
params.atomId,
|
||||
projection.endpoint.constraint,
|
||||
params.indexes.conformances,
|
||||
);
|
||||
if (!originMatches) {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-dependency-binding",
|
||||
`${path}.binding.projectionId`,
|
||||
`Projection ${edgeBinding.projectionId} cannot originate at receiver atom ${params.atomId}`,
|
||||
edgeBinding.via
|
||||
? `Projection ${edgeBinding.projectionId} cannot originate at the traversal target`
|
||||
: `Projection ${edgeBinding.projectionId} cannot originate at receiver atom ${params.atomId}`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
!constraintsEqual(requirement.target, projection.target.constraint) ||
|
||||
!constraintSatisfiesConstraint(
|
||||
projection.target.constraint,
|
||||
requirement.target,
|
||||
params.indexes.conformances,
|
||||
) ||
|
||||
requirement.cardinality !== projection.endpoint.cardinality
|
||||
) {
|
||||
issue(
|
||||
@@ -1290,8 +1397,21 @@ const validateConformances = (
|
||||
}
|
||||
validateAttachmentAccess(issues, attachment, conformance, `${bindingPath}.binding.edgeTypeId`);
|
||||
const projection = edgeProjection(attachment.attachment, binding.projectionId);
|
||||
const relationshipMember = operationEntry.member.kind === "relationship"
|
||||
? operationEntry.member
|
||||
: undefined;
|
||||
const operationEdge = relationshipMember
|
||||
? {
|
||||
...attachment.attachment,
|
||||
endpoints: attachment.attachment.endpoints.map((endpoint, index) =>
|
||||
index === (projection?.endpointIndex === 0 ? 1 : 0)
|
||||
? { ...endpoint, constraint: relationshipMember.target }
|
||||
: endpoint,
|
||||
) as [EdgeEndpoint, EdgeEndpoint],
|
||||
}
|
||||
: attachment.attachment;
|
||||
const expected = edgePrimitiveSignature(
|
||||
attachment.attachment,
|
||||
operationEdge,
|
||||
binding.projectionId,
|
||||
binding.primitive,
|
||||
);
|
||||
@@ -1320,7 +1440,11 @@ const validateConformances = (
|
||||
"Edge primitives may only implement relationship members",
|
||||
);
|
||||
} else if (
|
||||
!constraintsEqual(operationEntry.member.target, projection.target.constraint) ||
|
||||
!constraintSatisfiesConstraint(
|
||||
projection.target.constraint,
|
||||
operationEntry.member.target,
|
||||
indexes.conformances,
|
||||
) ||
|
||||
operationEntry.member.cardinality !== projection.endpoint.cardinality
|
||||
) {
|
||||
issue(
|
||||
@@ -1398,6 +1522,148 @@ const validateConformances = (
|
||||
});
|
||||
}
|
||||
|
||||
const materializedMembers = new Set<string>();
|
||||
for (const [materializationIndex, materialization] of
|
||||
(conformance.relationshipMaterializations ?? []).entries()) {
|
||||
const materializationPath =
|
||||
`${path}.relationshipMaterializations[${materializationIndex}]`;
|
||||
if (materializedMembers.has(materialization.memberId)) {
|
||||
issue(
|
||||
issues,
|
||||
"duplicate-relationship-materialization",
|
||||
materializationPath,
|
||||
`Relationship ${materialization.memberId} has more than one materialization recipe`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
materializedMembers.add(materialization.memberId);
|
||||
const member = interfaceEntry.revision.members.find((entry) =>
|
||||
entry.id === materialization.memberId);
|
||||
if (!member || member.kind !== "relationship") {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-relationship-materialization",
|
||||
`${materializationPath}.memberId`,
|
||||
`Member ${materialization.memberId} is not a relationship on ${conformance.interfaceRevisionId}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (member.cardinality !== "optional-one") {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-relationship-materialization",
|
||||
`${materializationPath}.memberId`,
|
||||
"A lazily constructed relationship must be optional-one before construction",
|
||||
);
|
||||
}
|
||||
const resolveOperation = member.operations.find((entry) =>
|
||||
entry.displayName === "resolve");
|
||||
const resolveBinding = resolveOperation
|
||||
? bindings.get(resolveOperation.id)
|
||||
: undefined;
|
||||
if (!resolveBinding || resolveBinding.kind !== "edge") {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-relationship-materialization",
|
||||
`${materializationPath}.memberId`,
|
||||
"A materialized relationship must bind its resolve operation to an edge",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (resolveBinding.edgeTypeId !== materialization.edgeTypeId) {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-relationship-materialization",
|
||||
`${materializationPath}.edgeTypeId`,
|
||||
"Materialization and relationship resolution must use the same edge",
|
||||
);
|
||||
}
|
||||
const attachment = findAttachment(indexes, "edge", materialization.edgeTypeId);
|
||||
if (!attachment || attachment.attachment.kind !== "edge") {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-relationship-materialization",
|
||||
`${materializationPath}.edgeTypeId`,
|
||||
`Unknown materialization edge ${materialization.edgeTypeId}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
validateAttachmentAccess(
|
||||
issues,
|
||||
attachment,
|
||||
conformance,
|
||||
`${materializationPath}.edgeTypeId`,
|
||||
);
|
||||
const hostProjection = edgeProjection(
|
||||
attachment.attachment,
|
||||
resolveBinding.projectionId,
|
||||
);
|
||||
const constructedProjection = edgeProjection(
|
||||
attachment.attachment,
|
||||
materialization.constructedProjectionId,
|
||||
);
|
||||
if (!hostProjection || !constructedProjection ||
|
||||
hostProjection.endpointIndex === constructedProjection.endpointIndex) {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-relationship-materialization",
|
||||
`${materializationPath}.constructedProjectionId`,
|
||||
"Materialization must connect through the opposite projection of the resolved edge",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!atomSatisfiesConstraint(
|
||||
materialization.constructorAtomId,
|
||||
member.target,
|
||||
indexes.conformances,
|
||||
)) {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-relationship-materialization",
|
||||
`${materializationPath}.constructorAtomId`,
|
||||
`Constructed atom ${materialization.constructorAtomId} does not satisfy the relationship target`,
|
||||
);
|
||||
}
|
||||
if (!atomSatisfiesConstraint(
|
||||
materialization.constructorAtomId,
|
||||
constructedProjection.endpoint.constraint,
|
||||
indexes.conformances,
|
||||
) || !atomSatisfiesConstraint(
|
||||
conformance.atomId,
|
||||
constructedProjection.target.constraint,
|
||||
indexes.conformances,
|
||||
)) {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-relationship-materialization",
|
||||
`${materializationPath}.constructedProjectionId`,
|
||||
"Constructed projection does not connect the constructed atom back to the host atom",
|
||||
);
|
||||
}
|
||||
const constructor = indexes.constructors.get(materialization.constructorAtomId);
|
||||
const constructorExport = constructor
|
||||
? indexes.packages.get(constructor.packageRevisionId)?.exports.get(constructor.exportId)
|
||||
: undefined;
|
||||
if (!constructor || !constructorExport || constructorExport.kind !== "constructor") {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-relationship-materialization",
|
||||
`${materializationPath}.constructorAtomId`,
|
||||
`Constructed atom ${materialization.constructorAtomId} has no valid constructor`,
|
||||
);
|
||||
} else if (!valueTypesEqual(
|
||||
constructorExport.inputType,
|
||||
valueType.atomRef(conformance.atomId),
|
||||
)) {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-relationship-materialization",
|
||||
`${materializationPath}.constructorAtomId`,
|
||||
`Component constructor must accept ${typeLabel(valueType.atomRef(conformance.atomId))}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const operation of interfaceEntry.operations.values()) {
|
||||
if (!bindings.has(operation.operation.id)) {
|
||||
issue(
|
||||
@@ -1714,9 +1980,15 @@ export const computeCapabilityClosure = (
|
||||
switch (dependency.binding.kind) {
|
||||
case "state":
|
||||
attachments.add(dependency.binding.slotId);
|
||||
if (dependency.binding.via) {
|
||||
attachments.add(dependency.binding.via.edgeTypeId);
|
||||
}
|
||||
break;
|
||||
case "edge":
|
||||
attachments.add(dependency.binding.edgeTypeId);
|
||||
if (dependency.binding.via) {
|
||||
attachments.add(dependency.binding.via.edgeTypeId);
|
||||
}
|
||||
break;
|
||||
case "receiver-interface":
|
||||
queued.push({
|
||||
@@ -1772,6 +2044,16 @@ export const computeCapabilityClosure = (
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const materialization of
|
||||
conformance.source.relationshipMaterializations ?? []) {
|
||||
attachments.add(materialization.edgeTypeId);
|
||||
constructors.add(materialization.constructorAtomId);
|
||||
const constructor = plan.constructors.get(materialization.constructorAtomId);
|
||||
if (constructor) {
|
||||
packages.add(constructor.packageRevisionId);
|
||||
includeDependencies(materialization.constructorAtomId, constructor.dependencies);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user