Add content-entity-reference well-known prop shape for code components
>>> [!note] Migrated issue <!-- Drupal.org comment --> <!-- Migrated from issue #3586613. --> Reported by: [penyaskito](https://www.drupal.org/user/959536) Related to !992 !980 >>> <h3 id="overview">Overview</h3> <p>Code components cannot declare a prop that receives a content entity object. Today an <em>entity reference prop</em> is only <em>implicit</em>: Canvas infers it from the presence of a key in <code>dataDependencies.entityFields</code> (see <span class="drupalorg-gitlab-issue-link drupalorg-gitlab-link-wrapper"><a href="https://git.drupalcode.org/project/canvas/-/work_items/3585298" class="drupalorg-gitlab-link">https://git.drupalcode.org/project/canvas/-/work_items/3585298</a></span>, the foundational issue this one is postponed on). There is no declared JSON Schema shape that says "this prop expects an entity of type X".</p> <p>This issue introduces that shape &mdash; <code>json-schema-definitions://canvas.module/content-entity-reference</code> &mdash; as a first-class well-known prop shape, alongside <code>image</code> and <code>video</code>. It is a direct prerequisite for the meta goal in <span class="drupalorg-gitlab-issue-link drupalorg-gitlab-link-wrapper"><a href="https://git.drupalcode.org/project/canvas/-/work_items/3573831" class="drupalorg-gitlab-link">https://git.drupalcode.org/project/canvas/-/work_items/3573831</a></span>.</p> <h4 id="why-required">Why is a declared shape required?</h4> <p>A reasonable question arises: <em>"Why not keep using bare <code>type: object</code> and infer entity-reference semantics from <code>dataDependencies.entityFields[&lt;prop&gt;]</code>?"</em> Four concrete reasons make that insufficient:</p> <ol> <li><strong>Shape matching must stay within the prop.</strong> <code>PropShape::normalizePropSchema()</code> and <code>JsonSchemaType::computeStorablePropShape()</code> route props to a <code>StorablePropShape</code> by shape identity (<code>type</code> + <code>$ref</code>). Without a <code>$ref</code>, routing would have to reach outside the prop into a sibling config key (<code>dataDependencies.entityFields</code>) to decide how to store it. That is cross-domain leakage and couples the shape matcher to config it does not own.</li> <li><strong>Validation becomes self-referential.</strong> Narrowing <code>SequenceKeysMustMatch</code> so <code>entityFields</code> keys only target entity-reference props (the <code>@todo</code> from <span class="drupalorg-gitlab-issue-link drupalorg-gitlab-link-wrapper"><a href="https://git.drupalcode.org/project/canvas/-/work_items/3585298" class="drupalorg-gitlab-link">https://git.drupalcode.org/project/canvas/-/work_items/3585298</a></span>) requires a way to identify which props <em>are</em> entity-reference props. Without a shape discriminator, the only signal is "does this prop have an <code>entityFields</code> entry?" &mdash; which is the thing being validated. The <code>$ref</code> breaks the cycle.</li> <li><strong>Multiple <code>type: object</code> props in one component.</strong> A component can legitimately have both an open-ended <code>type: object</code> prop and an entity-reference prop. Without a <code>$ref</code>, they are indistinguishable in props metadata, form rendering, tooling, and TypeScript codegen.</li> <li><strong>Author-facing contract.</strong> The SDC definition is consumed by tooling outside Canvas and by the JS component author. A bare <code>type: object</code> declares "this prop receives any object"; a <code>$ref</code> to <code>content-entity-reference</code> declares "this prop receives an entity object" &mdash; which is what Canvas will inject at render time. The schema should say what it means.</li> </ol> <p><code>image</code> and <code>video</code> are already well-known shapes via <code>$ref</code>; treating entity references the same way is the consistent choice.</p> <h3 id="proposed-resolution">Proposed resolution</h3> <ul> <li>Define <code>content-entity-reference</code> in <code>schema.json</code> and allow it in <code>canvas.json_schema.yml</code>. Deliberately omit <code>x-allowed-entity-type-id</code> / <code>x-allowed-bundle</code> from the allowed-key list so authors cannot hand-write them &mdash; typed-config rejects them as unsupported keys. Those constraints are derived from <code>dataDependencies.entityFields</code> and must not be duplicated.</li> <li>Add <code>JavaScriptComponent::getEntityReferenceProps()</code> and <code>::getEntityFieldExpressions()</code>. Shape identity comes from <code>JsonSchemaObjectRef::ContentEntityReference</code> &mdash; no duplicate constant on <code>JavaScriptComponent</code>.</li> <li>Extend <code>ComponentMetadataRequirementsChecker</code> to validate the shape on the projected SDC definition: not required, <code>x-allowed-entity-type-id</code> resolves to a content entity type, <code>x-allowed-bundle</code> required iff the entity type has bundles, and entity-reference props must not declare <code>examples</code> (the value is resolved at runtime from <code>entityFields</code>).</li> <li>Add a <code>conditions</code> option to <code>SequenceKeysMustMatchConstraint</code> so <code>entityFields</code> keys need only match entity-reference-shaped props, closing the <code>@todo</code> left behind by <span class="drupalorg-gitlab-issue-link drupalorg-gitlab-link-wrapper"><a href="https://git.drupalcode.org/project/canvas/-/work_items/3585298" class="drupalorg-gitlab-link">https://git.drupalcode.org/project/canvas/-/work_items/3585298</a></span>.</li> <li>Teach <code>JsonSchemaType::computeStorablePropShape()</code> to return a <code>StorablePropShape</code> for entity-reference shapes (<code>entity_reference</code> field type, <code>entity_reference_autocomplete</code> widget, <code>target_type</code> storage, optional <code>target_bundles</code>).</li> <li>Add <code>EntityFieldPropSourceMatcher::matchContentEntityReferenceShape() </code>matching host-entity reference fields whose target entity type (and bundle, if applicable) satisfies the shape.</li> <li><code>JavaScriptComponent::toSdcDefinition()</code> projects <code>x-allowed-entity-type-id</code> / <code>x-allowed-bundle</code> from <code>entityFields</code>, preserving a single source of truth.</li> <li><code>JsComponent::getExplicitInput() </code>reuses the parent-resolved <code>EvaluationResult</code> for each populated entity-reference prop, evaluates every declared <code>entityFields</code> expression on the referenced entity, and returns a payload keyed via <code>ContentEntityReferencePropKey::forExpression()</code>. Resolved values that aren't a <code>FieldableEntityInterface</code> (dangling reference, empty host field, or unresolvable source) are silently skipped.</li> </ul> <h3 id="remaining-tasks">Remaining tasks</h3> <ul> <li>Code review.</li> <li>Follow-up: <code>HostEntityPropSource</code> (source of truth "use the current host entity for this prop") tracked separately under <span class="drupalorg-gitlab-issue-link drupalorg-gitlab-link-wrapper"><a href="https://git.drupalcode.org/project/canvas/-/work_items/3573831" class="drupalorg-gitlab-link">https://git.drupalcode.org/project/canvas/-/work_items/3573831</a></span> (to be created).</li> </ul> <h3 id="api-changes">API changes</h3> <ul> <li>New well-known prop shape <code>json-schema-definitions://canvas.module/content-entity-reference</code>.</li> <li>New <code>JavaScriptComponent::getEntityReferenceProps()</code> and <code>::getEntityFieldExpressions()</code>.</li> <li>New <code>BetterEntityDataDefinition::getEntityType()</code>.</li> <li>New <code>ContentEntityReferencePropKey</code> class providing <code>::forExpression()</code> for developer-facing payload keys.</li> <li>New optional <code>conditions</code> property on <code>SequenceKeysMustMatchConstraint</code>.</li> <li>Removed: the ad-hoc "entity-ref props cannot be required" check in <code>JsComponentHasValidAndSupportedSdcMetadataConstraintValidator</code> (subsumed by <code>ComponentMetadataRequirementsChecker</code>'s shape branch).</li> </ul> <h3 id="data-model-changes">Data model changes</h3> <ul> <li><code>schema.json</code>: new <code>content-entity-reference</code> definition.</li> <li><code>canvas.json_schema.yml</code>: new <code>$ref</code> choice for object props; <code>x-allowed-*</code> keys intentionally not listed.</li> <li><code>canvas.schema.yml</code>: <code>dataDependencies.entityFields</code> subset check narrowed to entity-reference-shaped props.</li> <li>Additive only &mdash; no config update hook needed.</li> </ul> > Related issue: [Issue #3585298](https://www.drupal.org/node/3585298) > Related issue: [Issue #3587673](https://www.drupal.org/node/3587673) > Related issue: [Issue #3573831](https://www.drupal.org/node/3573831)
issue