From e8167e25eebaab17f68d5c3d4b5c7b0d766bf3f5 Mon Sep 17 00:00:00 2001
From: Al Munnings <17699-almunnings@users.noreply.drupalcode.org>
Date: Thu, 4 Jan 2024 06:18:13 +0000
Subject: [PATCH] Issue #3410650 by almunnings: Support dynamic entity
 reference module

---
 .../SchemaType/RouteEntityUnion.php           |  12 +-
 .../Plugin/GraphQLCompose/SchemaType/View.php |  18 +--
 phpstan-baseline.neon                         |   4 +
 .../DataProducer/EntityUnpublishedFilter.php  |  12 +-
 .../DataProducer/FieldEntityReference.php     | 148 ++++++++++++++++++
 .../FieldType/DynamicEntityReferenceItem.php  |  44 ++++++
 .../FieldType/EntityReferenceItem.php         |  70 +++------
 .../EntityReferenceRevisionsItem.php          |   9 --
 .../GraphQLCompose/FieldUnionInterface.php    |  10 +-
 src/Plugin/GraphQLCompose/FieldUnionTrait.php |  84 +++++++---
 .../GraphQLComposeEntityTypeBase.php          |  56 ++++---
 .../GraphQLCompose/SchemaType/ImageType.php   |   8 +-
 tests/src/Functional/EntityUnionTest.php      |   4 +-
 tests/src/Functional/UnsupportedTest.php      |   4 +-
 14 files changed, 349 insertions(+), 134 deletions(-)
 create mode 100644 src/Plugin/GraphQL/DataProducer/FieldEntityReference.php
 create mode 100644 src/Plugin/GraphQLCompose/FieldType/DynamicEntityReferenceItem.php

diff --git a/modules/graphql_compose_routes/src/Plugin/GraphQLCompose/SchemaType/RouteEntityUnion.php b/modules/graphql_compose_routes/src/Plugin/GraphQLCompose/SchemaType/RouteEntityUnion.php
index 8e6f490d..b3f12610 100644
--- a/modules/graphql_compose_routes/src/Plugin/GraphQLCompose/SchemaType/RouteEntityUnion.php
+++ b/modules/graphql_compose_routes/src/Plugin/GraphQLCompose/SchemaType/RouteEntityUnion.php
@@ -6,7 +6,6 @@ namespace Drupal\graphql_compose_routes\Plugin\GraphQLCompose\SchemaType;
 
 use Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeSchemaTypeBase;
 use Drupal\graphql_compose\Wrapper\EntityTypeWrapper;
-use GraphQL\Type\Definition\Type;
 use GraphQL\Type\Definition\UnionType;
 
 /**
@@ -24,13 +23,18 @@ class RouteEntityUnion extends GraphQLComposeSchemaTypeBase {
   public function getTypes(): array {
     $types = [];
 
+    $union_types = array_map(
+      fn(EntityTypeWrapper $bundle): string => $bundle->getTypeSdl(),
+      $this->getUnionBundles()
+    );
+
     $types[] = new UnionType([
       'name' => $this->getPluginId(),
       'description' => (string) $this->t('A list of possible entities that can be returned by URL.'),
       'types' => fn() => array_map(
-        fn(EntityTypeWrapper $bundle): Type => static::type($bundle->getTypeSdl()),
-        $this->getUnionBundles()
-      ) ?: [static::type('UnsupportedType')],
+        static::type(...),
+        $union_types ?: ['UnsupportedType']
+      ),
     ]);
 
     return $types;
diff --git a/modules/graphql_compose_views/src/Plugin/GraphQLCompose/SchemaType/View.php b/modules/graphql_compose_views/src/Plugin/GraphQLCompose/SchemaType/View.php
index 730e09a2..096e8102 100644
--- a/modules/graphql_compose_views/src/Plugin/GraphQLCompose/SchemaType/View.php
+++ b/modules/graphql_compose_views/src/Plugin/GraphQLCompose/SchemaType/View.php
@@ -170,9 +170,9 @@ class View extends GraphQLComposeSchemaTypeBase {
       'name' => 'ViewResultUnion',
       'description' => (string) $this->t('All available view result types.'),
       'types' => fn() => array_map(
-        fn(string $result_name): Type => static::type($result_name),
-        $union_types
-      ) ?: [static::type('UnsupportedType')],
+        static::type(...),
+        $union_types ?: ['UnsupportedType']
+      ),
     ]);
 
     return $types;
@@ -537,15 +537,13 @@ class View extends GraphQLComposeSchemaTypeBase {
     // Create exposed input for contextual filters.
     $contextual_filters = $display->getOption('arguments') ?: [];
 
-    $contextual_fields = array_map(
-      fn () => Type::string(),
-      $contextual_filters
-    );
-
-    if (!empty($contextual_fields)) {
+    if ($contextual_filters) {
       $types[] = new InputObjectType([
         'name' => $display->getGraphQlContextualFilterInputName(),
-        'fields' => fn() => $contextual_fields,
+        'fields' => fn() => array_map(
+          fn () => Type::string(),
+          $contextual_filters,
+        ),
       ]);
     }
 
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index ac9262aa..bb1cda78 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -19,3 +19,7 @@ parameters:
 			message: "#^Property Drupal\\\\graphql_compose\\\\Plugin\\\\GraphQLCompose\\\\FieldType\\\\ImageItem\\:\\:\\$sanitizer has unknown class enshrined\\\\svgSanitize\\\\Sanitizer as its type\\.$#"
 			count: 1
 			path: src/Plugin/GraphQLCompose/FieldType/ImageItem.php
+		-
+			message: "#^Call to static method getTargetTypes\\(\\) on an unknown class Drupal\\\\dynamic_entity_reference\\\\Plugin\\\\Field\\\\FieldType\\\\DynamicEntityReferenceItem\\.$#"
+			count: 1
+			path: src/Plugin/GraphQLCompose/FieldType/DynamicEntityReferenceItem.php
diff --git a/src/Plugin/GraphQL/DataProducer/EntityUnpublishedFilter.php b/src/Plugin/GraphQL/DataProducer/EntityUnpublishedFilter.php
index ce09147d..904270e3 100644
--- a/src/Plugin/GraphQL/DataProducer/EntityUnpublishedFilter.php
+++ b/src/Plugin/GraphQL/DataProducer/EntityUnpublishedFilter.php
@@ -12,7 +12,7 @@ use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
- * Remove unpublished entities from a results array.
+ * Remove unpublished entities (ignoring permissions) from a results array.
  *
  * @DataProducer(
  *   id = "entity_unpublished_filter",
@@ -53,7 +53,7 @@ class EntityUnpublishedFilter extends DataProducerPluginBase implements Containe
   }
 
   /**
-   * Remove unpublished entities from a results array.
+   * Remove unpublished entities (ignoring permissions) from a results array.
    *
    * @param array $results
    *   The results form a field plugin type to process.
@@ -61,12 +61,16 @@ class EntityUnpublishedFilter extends DataProducerPluginBase implements Containe
    *   The cache context.
    *
    * @return mixed
-   *   Results from resolution. Array for multiple.
+   *   The filtered results.
    */
   public function resolve(array $results, FieldContext $context) {
     $settings = $this->configFactory->get('graphql_compose.settings');
+    $exclude = $settings->get('settings.exclude_unpublished') ?: FALSE;
 
-    if ($settings->get('settings.exclude_unpublished')) {
+    // Don't exclude unpublished in preview mode.
+    $preview = $context->getContextValue('preview');
+
+    if ($exclude && !$preview) {
       $results = array_filter($results, function ($result) {
         return ($result instanceof EntityPublishedInterface)
           ? $result->isPublished()
diff --git a/src/Plugin/GraphQL/DataProducer/FieldEntityReference.php b/src/Plugin/GraphQL/DataProducer/FieldEntityReference.php
new file mode 100644
index 00000000..09f7b4a2
--- /dev/null
+++ b/src/Plugin/GraphQL/DataProducer/FieldEntityReference.php
@@ -0,0 +1,148 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\graphql_compose\Plugin\GraphQL\DataProducer;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\graphql\GraphQL\Execution\FieldContext;
+use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
+use Drupal\graphql\Plugin\GraphQL\DataProducer\Field\EntityReferenceTrait;
+use GraphQL\Deferred;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Produce the referenced entities from a field.
+ *
+ * Note: If you find a nice way to put this into a buffer, that can be used
+ * to resolve multiple entity types and revisions at once, please contrib!
+ *
+ * Deferred with referencedEntities covers a lot of use cases.
+ *
+ * @DataProducer(
+ *   id = "field_entity_reference",
+ *   name = @Translation("Field Entity Reference"),
+ *   description = @Translation("Return entity references from a field."),
+ *   produces = @ContextDefinition("mixed",
+ *     label = @Translation("Referenced entities"),
+ *   ),
+ *   consumes = {
+ *     "entity" = @ContextDefinition("entity",
+ *       label = @Translation("Entity instance"),
+ *     ),
+ *     "field" = @ContextDefinition("string",
+ *       label = @Translation("Field name"),
+ *     ),
+ *     "types" = @ContextDefinition("any",
+ *      label = @Translation("Entity types allowed to load"),
+ *      multiple = TRUE,
+ *    ),
+ *     "language" = @ContextDefinition("string",
+ *       label = @Translation("Language to use"),
+ *       required = FALSE,
+ *     ),
+ *   },
+ * )
+ */
+class FieldEntityReference extends DataProducerPluginBase implements ContainerFactoryPluginInterface {
+
+  use EntityReferenceTrait;
+
+  /**
+   * Constructs a new EntityLoadByUuidOrId instance.
+   *
+   * @param array $configuration
+   *   The plugin configuration.
+   * @param string $plugin_id
+   *   The plugin ID.
+   * @param mixed $plugin_definition
+   *   The plugin definition.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   The entity type manager.
+   */
+  public function __construct(
+    array $configuration,
+    string $plugin_id,
+    $plugin_definition,
+    protected EntityTypeManagerInterface $entityTypeManager
+  ) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager'),
+    );
+  }
+
+  /**
+   * Finds the requested field on the entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface|null $entity
+   *   The entity to resolve a field fields off.
+   * @param string $field
+   *   The field to resolve entities off.
+   * @param array $types
+   *   The union entity types allowed to load.
+   * @param string|null $language
+   *   The language to use.
+   * @param \Drupal\graphql\GraphQL\Execution\FieldContext $context
+   *   The field context.
+   *
+   * @return \GraphQL\Deferred|array
+   *   The resolves entities from the field.
+   */
+  public function resolve(?EntityInterface $entity, string $field, array $types, ?string $language, FieldContext $context): Deferred|array {
+
+    if (!$entity instanceof FieldableEntityInterface || !$entity->hasField($field)) {
+      return [];
+    }
+
+    $field = $entity->get($field);
+    if (!$field instanceof EntityReferenceFieldItemListInterface || !$field->access('view')) {
+      return [];
+    }
+
+    // Resolve the entities.
+    return new Deferred(function () use ($field, $types, $language, $context) {
+      $entities = $field->referencedEntities();
+
+      if ($language) {
+        $entities = $this->getTranslated($entities, $language);
+      }
+
+      foreach ($entities as $entity) {
+        $context->addCacheableDependency($entity);
+      }
+
+      $entities = $this->filterAccessible($entities, NULL, 'view', $context);
+
+      // Add a list cache tags for each empty type.
+      foreach ($types as $type) {
+        $has_entity = array_filter(
+          $entities,
+          fn ($entity) => $entity->getEntityTypeId() === $type
+        );
+
+        if (!$has_entity) {
+          $type = $this->entityTypeManager->getDefinition($type);
+          $tags = $type->getListCacheTags();
+          $context->addCacheTags($tags);
+        }
+      }
+
+      return $entities;
+    });
+  }
+
+}
diff --git a/src/Plugin/GraphQLCompose/FieldType/DynamicEntityReferenceItem.php b/src/Plugin/GraphQLCompose/FieldType/DynamicEntityReferenceItem.php
new file mode 100644
index 00000000..6978aa6b
--- /dev/null
+++ b/src/Plugin/GraphQLCompose/FieldType/DynamicEntityReferenceItem.php
@@ -0,0 +1,44 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\graphql_compose\Plugin\GraphQLCompose\FieldType;
+
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\dynamic_entity_reference\Plugin\Field\FieldType\DynamicEntityReferenceItem as DynamicEntityReferenceItemBase;
+
+/**
+ * {@inheritdoc}
+ *
+ * @GraphQLComposeFieldType(
+ *   id = "dynamic_entity_reference"
+ * )
+ */
+class DynamicEntityReferenceItem extends EntityReferenceItem {
+
+  /**
+   * {@inheritdoc}
+   *
+   * Force to be non-generic to get a unique union type.
+   */
+  public function isGenericUnion(): bool {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTypeSdl(): string {
+    return $this->getUnionTypeSdl();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getUnionTargetTypes(FieldDefinitionInterface $field_definition): array {
+    return DynamicEntityReferenceItemBase::getTargetTypes(
+      $field_definition->getSettings()
+    );
+  }
+
+}
diff --git a/src/Plugin/GraphQLCompose/FieldType/EntityReferenceItem.php b/src/Plugin/GraphQLCompose/FieldType/EntityReferenceItem.php
index c68fc246..379afa27 100644
--- a/src/Plugin/GraphQLCompose/FieldType/EntityReferenceItem.php
+++ b/src/Plugin/GraphQLCompose/FieldType/EntityReferenceItem.php
@@ -8,7 +8,6 @@ use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\TypedData\TranslatableInterface;
 use Drupal\graphql\GraphQL\Resolver\Composite;
 use Drupal\graphql\GraphQL\ResolverBuilder;
-use Drupal\graphql_compose\Plugin\GraphQL\DataProducer\FieldProducerTrait;
 use Drupal\graphql_compose\Plugin\GraphQLCompose\FieldUnionInterface;
 use Drupal\graphql_compose\Plugin\GraphQLCompose\FieldUnionTrait;
 use Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeFieldTypeBase;
@@ -23,61 +22,32 @@ use Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeFieldTypeBase;
 class EntityReferenceItem extends GraphQLComposeFieldTypeBase implements FieldUnionInterface {
 
   use FieldUnionTrait;
-  use FieldProducerTrait {
-    getProducers as getProducersTrait;
-  }
-
-  /**
-   * Producer buffer.
-   *
-   * Producer to use when not in a preview context.
-   *
-   * @var string
-   */
-  protected string $producerBuffer = 'entity_reference';
-
-  /**
-   * Producer property.
-   *
-   * Producer to use when in a preview context.
-   * Override the producer trait value property.
-   * Resolve direct from the entity field value.
-   *
-   * @var string
-   *
-   * @see \Drupal\graphql_compose\Plugin\GraphQL\DataProducer\FieldProducerPlugin::resolve()
-   */
-  public string $producerProperty = 'entity';
 
   /**
    * {@inheritdoc}
    */
   public function getProducers(ResolverBuilder $builder): Composite {
-    return $builder->compose(
-      $builder->cond([
-        [
-          // If in preview, resolve direct from the provided entity.
-          $builder->fromContext('preview'),
-          $this->getProducersTrait($builder),
-        ], [
-          // Use GraphQL module producers with buffers.
-          $builder->fromValue(TRUE),
-          $builder->compose(
-            $builder->produce($this->producerBuffer)
-              ->map('field', $builder->fromValue($this->getFieldName()))
-              ->map('entity', $builder->fromParent())
-              ->map('language', $builder->callback(
-                fn (EntityInterface $entity) => ($entity instanceof TranslatableInterface)
-                  ? $entity->language()->getId()
-                  : NULL
-              )),
 
-            // Remove unpublished entities (optional).
-            $builder->produce('entity_unpublished_filter')
-              ->map('value', $builder->fromParent()),
-          ),
-        ],
-      ]),
+    $field_name = $this->getFieldName();
+
+    $target_types = $this->getUnionTargetTypes(
+      $this->getFieldDefinition()
+    );
+
+    return $builder->compose(
+      $builder->produce('field_entity_reference')
+        ->map('entity', $builder->fromParent())
+        ->map('field', $builder->fromValue($field_name))
+        ->map('types', $builder->fromValue($target_types))
+        ->map('language', $builder->callback(
+          fn (EntityInterface $entity) => ($entity instanceof TranslatableInterface)
+            ? $entity->language()->getId()
+            : NULL
+        )),
+
+      // Optionally remove any unpublished references.
+      $builder->produce('entity_unpublished_filter')
+        ->map('value', $builder->fromParent()),
     );
   }
 
diff --git a/src/Plugin/GraphQLCompose/FieldType/EntityReferenceRevisionsItem.php b/src/Plugin/GraphQLCompose/FieldType/EntityReferenceRevisionsItem.php
index a914141e..4a051f93 100644
--- a/src/Plugin/GraphQLCompose/FieldType/EntityReferenceRevisionsItem.php
+++ b/src/Plugin/GraphQLCompose/FieldType/EntityReferenceRevisionsItem.php
@@ -13,13 +13,4 @@ namespace Drupal\graphql_compose\Plugin\GraphQLCompose\FieldType;
  */
 class EntityReferenceRevisionsItem extends EntityReferenceItem {
 
-  /**
-   * Producer buffer.
-   *
-   * Producer to use when not in preview.
-   *
-   * @var string
-   */
-  protected string $producerBuffer = 'entity_reference_revisions';
-
 }
diff --git a/src/Plugin/GraphQLCompose/FieldUnionInterface.php b/src/Plugin/GraphQLCompose/FieldUnionInterface.php
index dec60750..db60af1a 100644
--- a/src/Plugin/GraphQLCompose/FieldUnionInterface.php
+++ b/src/Plugin/GraphQLCompose/FieldUnionInterface.php
@@ -34,10 +34,14 @@ interface FieldUnionInterface {
   public function getUnionTypeSdl(): string;
 
   /**
-   * Get the target types for target type unions.
+   * Get the target schema types keyed by entity type and bundle.
    *
-   * @return array
-   *   Schema types available to union.
+   * The result is an array of types in the format: TYPE_ID:BUNDLE_ID => SDL
+   * EG: node:page => NodePage
+   *     user:user => User.
+   *
+   * @return string[]
+   *   Schema types available to union on the field.
    */
   public function getUnionTypeMapping(): array;
 
diff --git a/src/Plugin/GraphQLCompose/FieldUnionTrait.php b/src/Plugin/GraphQLCompose/FieldUnionTrait.php
index b1d5b201..770d935e 100644
--- a/src/Plugin/GraphQLCompose/FieldUnionTrait.php
+++ b/src/Plugin/GraphQLCompose/FieldUnionTrait.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace Drupal\graphql_compose\Plugin\GraphQLCompose;
 
+use Drupal\Core\Field\FieldDefinitionInterface;
+
 use function Symfony\Component\String\u;
 
 /**
@@ -109,13 +111,13 @@ trait FieldUnionTrait {
     }
 
     // Ensure we have something to map.
-    if (!$mapping = $this->getUnionTypeMapping()) {
+    if (!$union_mapping = $this->getUnionTypeMapping()) {
       return 'UnsupportedType';
     }
 
     // If single type, return first type configured.
     if ($this->isSingleUnion()) {
-      return reset($mapping);
+      return reset($union_mapping);
     }
 
     // Generate a new type for the field.
@@ -138,15 +140,9 @@ trait FieldUnionTrait {
    *   Schema types available to union.
    */
   public function getUnionTypeMapping(): array {
-    /** @var \Drupal\Core\Field\FieldDefinition $field_definition */
-    $field_definition = $this->getFieldDefinition();
-
-    if (!$field_definition) {
-      return [];
-    }
-
-    // Reduce lookups.
     $mapping = &drupal_static('graphql_compose_union_type_mapping', []);
+
+    $field_definition = $this->getFieldDefinition();
     $field_id = $field_definition->getUniqueIdentifier();
 
     if (isset($mapping[$field_id])) {
@@ -155,39 +151,79 @@ trait FieldUnionTrait {
 
     $mapping[$field_id] = [];
 
-    // Unknown target type.
-    if (!$target_type_id = $field_definition->getSetting('target_type')) {
-      return [];
+    $target_types = $this->getUnionTargetTypes($field_definition);
+    foreach ($target_types as $entity_type_id) {
+      $mapping[$field_id] += $this->getUnionTargetBundles($field_definition, $entity_type_id);
     }
 
-    // Entity type plugin not defined.
-    if (!$plugin_instance = $this->gqlEntityTypeManager->getPluginInstance($target_type_id)) {
+    return $mapping[$field_id];
+  }
+
+  /**
+   * Get the target types for target type unions.
+   *
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The field definition.
+   *
+   * @return string[]
+   *   The target entity types.
+   */
+  protected function getUnionTargetTypes(FieldDefinitionInterface $field_definition): array {
+    $target_types = $field_definition->getSetting('target_type');
+    return is_array($target_types) ? $target_types : [$target_types];
+  }
+
+  /**
+   * Get the target bundles for entity type.
+   *
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The field definition.
+   * @param string $entity_type_id
+   *   The entity type id.
+   *
+   * @return string[]
+   *   The target entity types available to map.
+   */
+  protected function getUnionTargetBundles(FieldDefinitionInterface $field_definition, string $entity_type_id) {
+    $result = [];
+
+    // This entity type is not supported by graphql compose.
+    $plugin_instance = $this->gqlEntityTypeManager->getPluginInstance($entity_type_id);
+    if (!$plugin_instance) {
       return [];
     }
 
     // Get the target configuration from the field.
     $handler_settings = $field_definition->getSetting('handler_settings');
+    if (!$handler_settings) {
+      // Look for type specific config (eg dynamic entity reference)
+      $type_settings = $field_definition->getSetting($entity_type_id);
+      $handler_settings = $type_settings['handler_settings'] ?? [];
+    }
+
+    $target_bundles = array_keys($handler_settings['target_bundles'] ?? [] ?: []);
+    $all_bundles = array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type_id));
 
-    $all_bundles = array_keys($this->entityTypeBundleInfo->getBundleInfo($target_type_id));
-    $target_bundles = array_keys($handler_settings['target_bundles'] ?? []);
+    // Some plugins allow you to negate your selection.
+    $negate = (bool) ($handler_settings['negate'] ?? FALSE);
 
-    // Paragraphs allows you to negate.
-    if (!empty($handler_settings['negate'])) {
+    if ($negate) {
+      // Get the opposite of the selected bundles.
       $target_bundles = array_diff($all_bundles, $target_bundles);
     }
     else {
-      // Assumption none is all.
+      // Use "all" if nothing selected.
       $target_bundles = $target_bundles ?: $all_bundles;
     }
 
     // Limit mapping to enabled bundles within the entity type plugin.
-    foreach ($target_bundles as $target_bundle_id) {
-      if ($target_bundle = $plugin_instance->getBundle($target_bundle_id)) {
-        $mapping[$field_id][$target_bundle_id] = $target_bundle->getTypeSdl();
+    foreach ($target_bundles as $bundle_id) {
+      if ($target_bundle = $plugin_instance->getBundle($bundle_id)) {
+        $result[$entity_type_id . ':' . $bundle_id] = $target_bundle->getTypeSdl();
       }
     }
 
-    return $mapping[$field_id];
+    return $result;
   }
 
 }
diff --git a/src/Plugin/GraphQLCompose/GraphQLComposeEntityTypeBase.php b/src/Plugin/GraphQLCompose/GraphQLComposeEntityTypeBase.php
index b7e47e58..e2b69b18 100644
--- a/src/Plugin/GraphQLCompose/GraphQLComposeEntityTypeBase.php
+++ b/src/Plugin/GraphQLCompose/GraphQLComposeEntityTypeBase.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
 namespace Drupal\graphql_compose\Plugin\GraphQLCompose;
 
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
@@ -265,16 +266,21 @@ abstract class GraphQLComposeEntityTypeBase extends PluginBase implements GraphQ
     }
 
     // Create generic entity wide union.
-    $union = new UnionType([
+    $union_types = array_map(
+      fn(EntityTypeWrapper $bundle): string => $bundle->getTypeSdl(),
+      $bundles
+    );
+
+    $entity_union = new UnionType([
       'name' => $this->getUnionTypeSdl(),
       'description' => $this->getDescription(),
       'types' => fn() => array_map(
-        fn($bundle): Type => $this->gqlSchemaTypeManager->get($bundle->getTypeSdl()),
-        $bundles
-      ) ?: [$this->gqlSchemaTypeManager->get('UnsupportedType')],
+        $this->gqlSchemaTypeManager->get(...),
+        $union_types ?: ['UnsupportedType']
+      ),
     ]);
 
-    $this->gqlSchemaTypeManager->add($union);
+    $this->gqlSchemaTypeManager->add($entity_union);
 
     // Create generic entity wide query.
     $enabled_query_bundles = array_filter(
@@ -284,7 +290,9 @@ abstract class GraphQLComposeEntityTypeBase extends PluginBase implements GraphQ
 
     if ($this->isQueryLoadSimple() && $enabled_query_bundles) {
       // Entities without bundles shouldn't return a union.
-      $query_type = $entity_type_has_bundles ? $this->getUnionTypeSdl() : $this->getTypeSdl();
+      $query_type = $entity_type_has_bundles
+        ? $this->getUnionTypeSdl()
+        : $this->getTypeSdl();
 
       $entityQuery = new ObjectType([
         'name' => 'Query',
@@ -331,7 +339,7 @@ abstract class GraphQLComposeEntityTypeBase extends PluginBase implements GraphQ
       'name' => $bundle->getTypeSdl(),
       'description' => $bundle->getDescription() ?: $this->getDescription(),
       'interfaces' => fn() => array_map(
-        fn($interface): Type => $this->gqlSchemaTypeManager->get($interface),
+        $this->gqlSchemaTypeManager->get(...),
         $this->getInterfaces()
       ),
       'fields' => function () use ($fields) {
@@ -382,9 +390,8 @@ abstract class GraphQLComposeEntityTypeBase extends PluginBase implements GraphQ
       $this->gqlSchemaTypeManager->extend($entityQuery);
     }
 
-    // Add union types for non-simple unions.
+    // Add per-field union types.
     foreach ($fields as $field_plugin) {
-      // Check it uses the union trait.
       if (!$field_plugin instanceof FieldUnionInterface) {
         continue;
       }
@@ -404,14 +411,13 @@ abstract class GraphQLComposeEntityTypeBase extends PluginBase implements GraphQ
         continue;
       }
 
-      // Create the new union type.
       $union = new UnionType([
         'name' => $field_plugin->getUnionTypeSdl(),
         'description' => $field_plugin->getDescription(),
         'types' => fn() => array_map(
-          fn($type): Type => $this->gqlSchemaTypeManager->get($type),
-          $field_plugin->getUnionTypeMapping()
-        ) ?: [$this->gqlSchemaTypeManager->get('UnsupportedType')],
+          $this->gqlSchemaTypeManager->get(...),
+          $field_plugin->getUnionTypeMapping() ?: ['UnsupportedType']
+        ),
       ]);
 
       $this->gqlSchemaTypeManager->add($union);
@@ -473,7 +479,7 @@ abstract class GraphQLComposeEntityTypeBase extends PluginBase implements GraphQ
           }
         }
 
-        throw new UserError(sprintf('Could not resolve entity of type %s, is it enabled in the schema?', $entity_class));
+        throw new UserError(sprintf('Could not resolve union for %s', $entity_class));
       }
     );
 
@@ -541,25 +547,25 @@ abstract class GraphQLComposeEntityTypeBase extends PluginBase implements GraphQ
       }
 
       // Generic unions return a generic entity union.
-      if ($field_plugin->isGenericUnion()) {
-        continue;
-      }
-
       // Single unions just return the type.
-      if ($field_plugin->isSingleUnion()) {
+      if ($field_plugin->isGenericUnion() || $field_plugin->isSingleUnion()) {
         continue;
       }
 
-      $map = $field_plugin->getUnionTypeMapping();
-
       $registry->addTypeResolver(
         $field_plugin->getUnionTypeSdl(),
-        function ($value) use ($entity_class, $map) {
-          if (array_key_exists($value->bundle(), $map)) {
-            return $map[$value->bundle()];
+        function (?EntityInterface $value) use ($field_plugin) {
+          $entity_type_id = $value?->getEntityTypeId();
+          $entity_bundle_id = $value?->bundle();
+
+          $union_map = $entity_type_id . ':' . $entity_bundle_id;
+          $union_mapping = $field_plugin->getUnionTypeMapping();
+
+          if (array_key_exists($union_map, $union_mapping)) {
+            return $union_mapping[$union_map];
           }
 
-          throw new UserError(sprintf('Could not resolve entity of type %s::%s for this field, is it enabled in the field config?', $entity_class, $value->bundle()));
+          throw new UserError(sprintf('Could not resolve union mapping %s:%s', $entity_type_id, $entity_bundle_id));
         }
       );
     }
diff --git a/src/Plugin/GraphQLCompose/SchemaType/ImageType.php b/src/Plugin/GraphQLCompose/SchemaType/ImageType.php
index ee10d6c6..f2f76f16 100644
--- a/src/Plugin/GraphQLCompose/SchemaType/ImageType.php
+++ b/src/Plugin/GraphQLCompose/SchemaType/ImageType.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
 
 namespace Drupal\graphql_compose\Plugin\GraphQLCompose\SchemaType;
 
+use Drupal\Core\StringTranslation\ByteSizeMarkup;
 use Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeSchemaTypeBase;
 use GraphQL\Type\Definition\ObjectType;
 use GraphQL\Type\Definition\Type;
@@ -63,10 +64,15 @@ class ImageType extends GraphQLComposeSchemaTypeBase {
         if ($config->get('settings.svg_image')) {
           $svg_max = (int) $config->get('settings.svg_filesize') ?: 100;
 
+          // format_size is deprecated in drupal:10.2.0.
+          $svg_max = (floatval(\Drupal::VERSION) >= 10.2)
+            ? ByteSizeMarkup::create($svg_max * 1024)
+            : format_size($svg_max * 1024); /* @phpstan-ignore-line */
+
           $fields['svg'] = [
             'type' => Type::string(),
             'description' => (string) $this->t('Contents of the image, if the mime is `image/svg+xml` and size <= `@size`.', [
-              '@size' => format_size($svg_max * 1024),
+              '@size' => $svg_max,
             ]),
           ];
         }
diff --git a/tests/src/Functional/EntityUnionTest.php b/tests/src/Functional/EntityUnionTest.php
index fc4e1fcc..a2fb4b06 100644
--- a/tests/src/Functional/EntityUnionTest.php
+++ b/tests/src/Functional/EntityUnionTest.php
@@ -7,7 +7,7 @@ namespace Drupal\Tests\graphql_compose\Functional;
 use Drupal\media\Entity\Media;
 use Drupal\media\MediaInterface;
 use Drupal\node\NodeInterface;
-use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
+use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
 use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
 
 /**
@@ -18,7 +18,7 @@ use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
 class EntityUnionTest extends GraphQLComposeBrowserTestBase {
 
   use MediaTypeCreationTrait;
-  use EntityReferenceTestTrait;
+  use EntityReferenceFieldCreationTrait;
 
   /**
    * The test node.
diff --git a/tests/src/Functional/UnsupportedTest.php b/tests/src/Functional/UnsupportedTest.php
index 78b7fce7..ef2b6fb0 100644
--- a/tests/src/Functional/UnsupportedTest.php
+++ b/tests/src/Functional/UnsupportedTest.php
@@ -7,7 +7,7 @@ namespace Drupal\Tests\graphql_compose\Functional;
 use Drupal\media\Entity\Media;
 use Drupal\media\MediaInterface;
 use Drupal\node\NodeInterface;
-use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
+use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
 use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
 
 /**
@@ -18,7 +18,7 @@ use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
 class UnsupportedTest extends GraphQLComposeBrowserTestBase {
 
   use MediaTypeCreationTrait;
-  use EntityReferenceTestTrait;
+  use EntityReferenceFieldCreationTrait;
 
   /**
    * The test node.
-- 
GitLab