From b5ae210faa6d09a881b29df604b38e9cadc1e6fd Mon Sep 17 00:00:00 2001
From: Theodoros Ploumis <me@theodorosploumis.com>
Date: Thu, 13 Feb 2025 14:01:52 +0200
Subject: [PATCH 01/11] Issue #3458995 | Initial commit -f module
 graphql_compose_mutations by theodorosploumis

---
 modules/graphql_compose_mutations/README.md   |  10 +
 .../graphql/generic_mutation.base.graphqls    | 105 +++
 .../generic_mutation.extension.graphqls       |   6 +
 .../operations_by_entity_type.base.graphqls   |   4 +
 ...erations_by_entity_type.extension.graphqls |   8 +
 .../graphql/permissions.base.graphqls         |  14 +
 .../graphql/permissions.extension.graphqls    |  14 +
 .../graphql_compose_mutations.info.yml        |   9 +
 .../graphql_compose_mutations.module          | 156 ++++
 .../graphql_compose_mutations.services.yml    |   9 +
 .../UserTokenInvalidationSubscriber.php       |  41 ++
 .../Response/GenericEntityResponse.php        |  78 ++
 .../OperationsByEntityTypeResponse.php        |  41 ++
 .../GraphQL/Response/PermissionsResponse.php  |  47 ++
 .../DataProducer/GenericMutationProducer.php  | 436 +++++++++++
 .../OperationsByEntityTypeProducer.php        | 157 ++++
 .../DataProducer/PermissionsProducer.php      | 174 +++++
 .../GenericMutationExtension.php              |  56 ++
 .../OperationsByEntityTypeExtension.php       |  50 ++
 .../SchemaExtension/PermissionsExtension.php  |  54 ++
 .../src/Services/UserPermissions.php          | 686 ++++++++++++++++++
 21 files changed, 2155 insertions(+)
 create mode 100644 modules/graphql_compose_mutations/README.md
 create mode 100644 modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls
 create mode 100644 modules/graphql_compose_mutations/graphql/generic_mutation.extension.graphqls
 create mode 100644 modules/graphql_compose_mutations/graphql/operations_by_entity_type.base.graphqls
 create mode 100644 modules/graphql_compose_mutations/graphql/operations_by_entity_type.extension.graphqls
 create mode 100644 modules/graphql_compose_mutations/graphql/permissions.base.graphqls
 create mode 100644 modules/graphql_compose_mutations/graphql/permissions.extension.graphqls
 create mode 100644 modules/graphql_compose_mutations/graphql_compose_mutations.info.yml
 create mode 100644 modules/graphql_compose_mutations/graphql_compose_mutations.module
 create mode 100644 modules/graphql_compose_mutations/graphql_compose_mutations.services.yml
 create mode 100644 modules/graphql_compose_mutations/src/EventSubscriber/UserTokenInvalidationSubscriber.php
 create mode 100644 modules/graphql_compose_mutations/src/GraphQL/Response/GenericEntityResponse.php
 create mode 100644 modules/graphql_compose_mutations/src/GraphQL/Response/OperationsByEntityTypeResponse.php
 create mode 100644 modules/graphql_compose_mutations/src/GraphQL/Response/PermissionsResponse.php
 create mode 100644 modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
 create mode 100644 modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/OperationsByEntityTypeProducer.php
 create mode 100644 modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/PermissionsProducer.php
 create mode 100644 modules/graphql_compose_mutations/src/Plugin/GraphQL/SchemaExtension/GenericMutationExtension.php
 create mode 100644 modules/graphql_compose_mutations/src/Plugin/GraphQL/SchemaExtension/OperationsByEntityTypeExtension.php
 create mode 100644 modules/graphql_compose_mutations/src/Plugin/GraphQL/SchemaExtension/PermissionsExtension.php
 create mode 100644 modules/graphql_compose_mutations/src/Services/UserPermissions.php

diff --git a/modules/graphql_compose_mutations/README.md b/modules/graphql_compose_mutations/README.md
new file mode 100644
index 00000000..a9be8596
--- /dev/null
+++ b/modules/graphql_compose_mutations/README.md
@@ -0,0 +1,10 @@
+# Generic Mutations for graphql_compose
+
+## Available GraphQL mutations
+
+- genericMutation
+
+## Available GraphQL queries
+
+- operationsByEntityType
+- permissions
diff --git a/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls b/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls
new file mode 100644
index 00000000..f5606373
--- /dev/null
+++ b/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls
@@ -0,0 +1,105 @@
+"""
+Helper input for String values.
+"""
+input keyValueString {
+  field: String!
+  value: String
+}
+
+"""
+Metadata values.
+"""
+input genericMetadata {
+  """
+  The entity type (node, user, profile, group etc).
+  """
+  entity_type: EntityType!
+
+  """
+  The bundle of the entity. Actually, we do not need this on DELETE.
+  """
+  entity_bundle: String!
+
+  """
+  The operation to perform
+  """
+  operation: Operation!
+
+  """
+  The id of the entity (if action is not CREATE)
+  """
+  id: Int
+
+  """
+  The parent entity type (node, user, profile, group etc) if exists.
+  """
+  parent_entity_type: EntityType
+
+  """
+  The parent entity ID if exists.
+  """
+  parent_entity_id: Int
+}
+
+"""
+The mutation available operations
+"""
+enum Operation {
+  DEFAULT
+  CREATE
+  UPDATE
+  DELETE
+}
+
+"""
+The mutation available entity types
+"""
+enum EntityType {
+  ACTIVITY
+  COMMENT
+  CONTACT_MESSAGE
+  EVENT_ENROLLMENT
+  FLAGGING
+  GROUP
+  GROUP_CONTENT
+  MESSAGE
+  NODE
+  POST
+  PRIVATE_MESSAGE
+  PRIVATE_MESSAGE_THREAD
+  PROFILE
+  TAXONOMY_TERM
+  USER
+  VOTE
+}
+
+"""
+A generic object containing any Drupal entity all values
+"""
+type AnyEntity {
+  id: Int
+  entity_type: String
+  entity_bundle: String
+  values: Json
+}
+
+type GenericEntityResponse implements Response {
+  errors: [Violation]
+  success: Boolean
+  entity: AnyEntity
+}
+
+scalar Violation
+
+"""
+The Json FieldType
+See graphql_compose/src/Plugin/GraphQLCompose/FieldType/JsonItem.php
+"""
+scalar Json
+
+"""
+ToDo: There is already a graphql Response... Why do we need this?
+"""
+interface Response {
+  errors: [Violation]
+}
diff --git a/modules/graphql_compose_mutations/graphql/generic_mutation.extension.graphqls b/modules/graphql_compose_mutations/graphql/generic_mutation.extension.graphqls
new file mode 100644
index 00000000..912a47c0
--- /dev/null
+++ b/modules/graphql_compose_mutations/graphql/generic_mutation.extension.graphqls
@@ -0,0 +1,6 @@
+extend type Mutation {
+  """
+  Generic entity mutation
+  """
+  genericMutation(data: Json!, metadata: genericMetadata!): GenericEntityResponse
+}
diff --git a/modules/graphql_compose_mutations/graphql/operations_by_entity_type.base.graphqls b/modules/graphql_compose_mutations/graphql/operations_by_entity_type.base.graphqls
new file mode 100644
index 00000000..27879eeb
--- /dev/null
+++ b/modules/graphql_compose_mutations/graphql/operations_by_entity_type.base.graphqls
@@ -0,0 +1,4 @@
+type OperationsByEntityTypeResponse implements Response {
+  errors: [Violation]
+  operations: Json
+}
diff --git a/modules/graphql_compose_mutations/graphql/operations_by_entity_type.extension.graphqls b/modules/graphql_compose_mutations/graphql/operations_by_entity_type.extension.graphqls
new file mode 100644
index 00000000..33f944c7
--- /dev/null
+++ b/modules/graphql_compose_mutations/graphql/operations_by_entity_type.extension.graphqls
@@ -0,0 +1,8 @@
+extend type Query {
+  """
+  Query to get operations by entity_type
+  """
+  operationsByEntityType(
+    entity_type: EntityType!,
+  ): OperationsByEntityTypeResponse
+}
diff --git a/modules/graphql_compose_mutations/graphql/permissions.base.graphqls b/modules/graphql_compose_mutations/graphql/permissions.base.graphqls
new file mode 100644
index 00000000..adcd6067
--- /dev/null
+++ b/modules/graphql_compose_mutations/graphql/permissions.base.graphqls
@@ -0,0 +1,14 @@
+"""
+The permissions available operations, extended
+"""
+enum OperationExtended {
+  VIEW
+  CREATE
+  UPDATE
+  DELETE
+}
+
+type PermissionsResponse implements Response {
+  errors: [Violation]
+  success: Boolean
+}
diff --git a/modules/graphql_compose_mutations/graphql/permissions.extension.graphqls b/modules/graphql_compose_mutations/graphql/permissions.extension.graphqls
new file mode 100644
index 00000000..feba89b1
--- /dev/null
+++ b/modules/graphql_compose_mutations/graphql/permissions.extension.graphqls
@@ -0,0 +1,14 @@
+extend type Query {
+  """
+  Query to get operation permissions by uid
+  """
+  permissions(
+    operation: OperationExtended!,
+    uid: String!,
+    entity_id: String,
+    entity_type: EntityType!,
+    entity_bundle: String!,
+    parent_entity_type: EntityType,
+    parent_entity_id: String,
+  ): PermissionsResponse
+}
diff --git a/modules/graphql_compose_mutations/graphql_compose_mutations.info.yml b/modules/graphql_compose_mutations/graphql_compose_mutations.info.yml
new file mode 100644
index 00000000..32e98101
--- /dev/null
+++ b/modules/graphql_compose_mutations/graphql_compose_mutations.info.yml
@@ -0,0 +1,9 @@
+name: "GraphQL Compose: Mutations"
+description: "Enable mutations for any Entity to the schema."
+type: module
+core_version_requirement: ^10.1 || ^11
+php: 8.1
+dependencies:
+  - graphql:graphql
+  - graphql_compose:graphql_compose
+package: GraphQL Compose
diff --git a/modules/graphql_compose_mutations/graphql_compose_mutations.module b/modules/graphql_compose_mutations/graphql_compose_mutations.module
new file mode 100644
index 00000000..f449561e
--- /dev/null
+++ b/modules/graphql_compose_mutations/graphql_compose_mutations.module
@@ -0,0 +1,156 @@
+<?php
+
+/**
+ * @file
+ * Extends module graphql_compose_mutations.
+ */
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\graphql\GraphQL\Execution\FieldContext;
+use Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeFieldTypeInterface;
+
+/**
+ * Implements hook_entity_base_field_info().
+ */
+function graphql_compose_mutations_entity_base_field_info(EntityTypeInterface $entity_type): ?array {
+  $fields = [];
+  $machine_names = graphql_compose_mutations_get_machine_names();
+
+  foreach ($machine_names as $machine_name) {
+    $field_type = str_contains($machine_name, "_user_") ? "boolean" : "string";
+    $fields[$machine_name] = BaseFieldDefinition::create($field_type)
+      ->setLabel(t('System field value of @name', ["@name" => $machine_name]))
+      ->setDescription(t('System field value of @name field', ["@name" => $machine_name]))
+      ->setComputed(TRUE)
+      ->setRequired(FALSE)
+      ->setReadOnly(TRUE)
+      ->setDefaultValue([]);
+  }
+
+  return $fields;
+}
+
+/**
+ * Implements hook_graphql_compose_field_results_alter().
+ */
+function graphql_compose_mutations_graphql_compose_field_results_alter(array &$results, $entity, GraphQLComposeFieldTypeInterface $plugin, FieldContext $context):void {
+  $field_definition = $plugin->getFieldDefinition();
+  $field_name = $field_definition->getName();
+  $entity_type_id = $entity->getEntityTypeId();
+  $current_uid = Drupal::currentUser()->id();
+
+  if ($entity instanceof EntityInterface) {
+    $uid = "";
+    $parent_entity_type = "";
+    $parent_entity_id = "";
+
+    // Common case of author.
+    if (property_exists($entity, "uid") && $entity->get("uid")->getValue()) {
+      if (isset($entity->get("uid")->getValue()[0])) {
+        $uid = $entity->get("uid")->getValue()[0]["target_id"];
+      }
+    }
+    // Entity type activity, post, vote.
+    if (in_array($entity_type_id, ["activity", "post", "vote"]) && property_exists($entity, "user_id") && $entity->get("user_id")->getValue()) {
+      if (isset($entity->get("user_id")->getValue()[0])) {
+        $uid = $entity->get("user_id")->getValue()[0]["target_id"];
+      }
+    }
+    // Entity type private_message
+    // private_message_thread has "members" array to get participants.
+    if ($entity_type_id === "private_message" && property_exists($entity, "owner") && $entity->get("owner")->getValue()) {
+      if (isset($entity->get("owner")->getValue()[0])) {
+        $uid = $entity->get("owner")->getValue()[0]["target_id"];
+      }
+    }
+
+    $entity_id = $entity->id();
+    $system_entity_type = $entity->getEntityTypeId();
+    $system_entity_bundle = $entity->bundle();
+    $system_entity_bundle_object = \Drupal::service('entity_type.bundle.info')->getBundleInfo($system_entity_type);
+
+    if (isset($system_entity_bundle_object[$system_entity_bundle])) {
+      $system_entity_bundle_label = $system_entity_bundle_object[$system_entity_bundle]['label'];
+    }
+    else {
+      $system_entity_bundle_label = "";
+    }
+
+    // Some entities need the Parent to check for Permissions.
+    if ($entity_type_id === "vote") {
+      $parent_entity_type = $$entity->get("entity_type")->toString();
+      $parent_entity_id = $$entity->get("entity_id")->toString();
+    }
+
+    // Get Permissions to update, delete current entity.
+    $permissions = \Drupal::service("graphql_compose_mutations.user_permissions");
+    $permissions_update = $permissions->userCanDoActionOnEntityByType(
+      "update",
+      $current_uid,
+      $entity_id,
+      $system_entity_type,
+      $system_entity_bundle,
+      $parent_entity_type,
+      $parent_entity_id
+    );
+    $permissions_delete = $permissions->userCanDoActionOnEntityByType(
+      "delete",
+      $current_uid,
+      $entity_id,
+      $system_entity_type,
+      $system_entity_bundle,
+      $parent_entity_type,
+      $parent_entity_id
+    );
+
+    $values = [
+      "system_entity_id" => $entity->id(),
+      "system_entity_type" => $system_entity_type,
+      "system_entity_bundle" => $system_entity_bundle,
+      "system_entity_bundle_label" => $system_entity_bundle_label,
+      "system_entity_user_is_owner" => $current_uid == $uid,
+      "system_entity_user_has_update_access" => $permissions_update["access"],
+      "system_entity_user_has_delete_access" => $permissions_delete["access"],
+    ];
+
+    $machine_names = graphql_compose_mutations_get_machine_names();
+    foreach ($machine_names as $machine_name) {
+      if ($field_name === $machine_name) {
+        $results = [$values[$machine_name]];
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_graphql_compose_entity_base_fields_alter().
+ */
+function graphql_compose_mutations_graphql_compose_entity_base_fields_alter(array &$fields, string $entity_type_id): void {
+  $machine_names = graphql_compose_mutations_get_machine_names();
+
+  foreach ($machine_names as $machine_name) {
+    $fields[$machine_name] = [
+      "required" => FALSE,
+    ];
+  }
+}
+
+/**
+ * Return the available machine names.
+ *
+ * @return array
+ *   The array of the fields.
+ */
+function graphql_compose_mutations_get_machine_names(): array {
+  return [
+    "system_entity_id",
+    "system_entity_type",
+    "system_entity_bundle",
+    "system_entity_bundle_label",
+    "system_entity_user_is_owner",
+    "system_entity_user_has_update_access",
+    "system_entity_user_has_delete_access",
+  ];
+}
diff --git a/modules/graphql_compose_mutations/graphql_compose_mutations.services.yml b/modules/graphql_compose_mutations/graphql_compose_mutations.services.yml
new file mode 100644
index 00000000..04115f07
--- /dev/null
+++ b/modules/graphql_compose_mutations/graphql_compose_mutations.services.yml
@@ -0,0 +1,9 @@
+services:
+  graphql_compose_mutations.user_token_subscriber:
+    class: Drupal\graphql_compose_mutations\EventSubscriber\UserTokenInvalidationSubscriber
+    tags:
+      - { name: event_subscriber }
+  graphql_compose_mutations.user_permissions:
+    class: Drupal\graphql_compose_mutations\Services\UserPermissions
+    arguments:
+      [ '@module_handler','@current_user', '@entity_type.manager', '@logger.channel.graphql', '@entity_type.bundle.info' ]
diff --git a/modules/graphql_compose_mutations/src/EventSubscriber/UserTokenInvalidationSubscriber.php b/modules/graphql_compose_mutations/src/EventSubscriber/UserTokenInvalidationSubscriber.php
new file mode 100644
index 00000000..b229423d
--- /dev/null
+++ b/modules/graphql_compose_mutations/src/EventSubscriber/UserTokenInvalidationSubscriber.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\graphql_compose_mutations\EventSubscriber;
+
+use Drupal\simple_oauth\Event\UserUpdateTokenInvalidationEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Event subscriber for user token invalidation events.
+ *
+ * Stolen from https://dgo.to/2946882#comment-15751851
+ */
+class UserTokenInvalidationSubscriber implements EventSubscriberInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents(): array {
+    return [
+      UserUpdateTokenInvalidationEvent::class => 'onUserUpdateTokenInvalidation',
+    ];
+  }
+
+  /**
+   * Reacts to the UserUpdateTokenInvalidationEvent.
+   *
+   * @param \Drupal\simple_oauth\Event\UserUpdateTokenInvalidationEvent $event
+   *   The event object.
+   */
+  public function onUserUpdateTokenInvalidation(UserUpdateTokenInvalidationEvent $event): void {
+    // TRUE if the user's roles, password or status has changed.
+    $shouldInvalidate = $event->haveUserAccessCharacteristicsChanged();
+    $event->setInvalidateAccessTokens($shouldInvalidate);
+
+    if ($shouldInvalidate) {
+      // Invalidate refresh tokens when access characteristics change.
+      $event->setInvalidateRefreshTokens(TRUE);
+    }
+  }
+
+}
diff --git a/modules/graphql_compose_mutations/src/GraphQL/Response/GenericEntityResponse.php b/modules/graphql_compose_mutations/src/GraphQL/Response/GenericEntityResponse.php
new file mode 100644
index 00000000..3821058c
--- /dev/null
+++ b/modules/graphql_compose_mutations/src/GraphQL/Response/GenericEntityResponse.php
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\graphql_compose_mutations\GraphQL\Response;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\graphql\GraphQL\Response\Response;
+
+/**
+ * Type of response used when a generic entity is returned.
+ */
+class GenericEntityResponse extends Response {
+
+  /**
+   * The entity to be served.
+   *
+   * @var \Drupal\Core\Entity\EntityInterface|null
+   */
+  protected ?EntityInterface $genericEntity = NULL;
+
+  /**
+   * The success value to be served.
+   *
+   * @var bool
+   */
+  protected bool $success = FALSE;
+
+  /**
+   * Sets the entity data.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface|null $genericEntity
+   *   The entity to be served.
+   */
+  public function setGenericEntity(?EntityInterface $genericEntity): void {
+    $this->genericEntity = $genericEntity;
+  }
+
+  /**
+   * Gets the array values to be served.
+   *
+   * @return array|null
+   *   The array values to be served.
+   */
+  public function getGenericEntity(): ?array {
+    $entity = $this->genericEntity;
+    if ($entity) {
+      return [
+        "values" => $entity->toArray(),
+        "id" => $entity->id(),
+        "entity_type" => $entity->getEntityTypeId(),
+        "entity_bundle" => $entity->bundle(),
+      ];
+    }
+    return NULL;
+  }
+
+  /**
+   * Gets the success.
+   *
+   * @return bool
+   *   The success to be served.
+   */
+  public function getSuccess(): bool {
+    return $this->success;
+  }
+
+  /**
+   * Sets the success.
+   *
+   * @param bool $success
+   *   The success value.
+   */
+  public function setSuccess(bool $success): void {
+    $this->success = $success;
+  }
+
+}
diff --git a/modules/graphql_compose_mutations/src/GraphQL/Response/OperationsByEntityTypeResponse.php b/modules/graphql_compose_mutations/src/GraphQL/Response/OperationsByEntityTypeResponse.php
new file mode 100644
index 00000000..7337efaf
--- /dev/null
+++ b/modules/graphql_compose_mutations/src/GraphQL/Response/OperationsByEntityTypeResponse.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\graphql_compose_mutations\GraphQL\Response;
+
+use Drupal\graphql\GraphQL\Response\Response;
+
+/**
+ * Type of response used when an OperationsByEntityType is returned.
+ */
+class OperationsByEntityTypeResponse extends Response {
+
+  /**
+   * The operations to be served.
+   *
+   * @var array|null
+   */
+  protected ?array $operations;
+
+  /**
+   * Sets the entity data.
+   *
+   * @param array|null $operations
+   *   The operations to be served.
+   */
+  public function setOperations(?array $operations): void {
+    $this->operations = $operations;
+  }
+
+  /**
+   * Gets the array values to be served.
+   *
+   * @return array|null
+   *   The array values to be served.
+   */
+  public function getOperations(): ?array {
+    return $this->operations ?? NULL;
+  }
+
+}
diff --git a/modules/graphql_compose_mutations/src/GraphQL/Response/PermissionsResponse.php b/modules/graphql_compose_mutations/src/GraphQL/Response/PermissionsResponse.php
new file mode 100644
index 00000000..e0889a9d
--- /dev/null
+++ b/modules/graphql_compose_mutations/src/GraphQL/Response/PermissionsResponse.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\graphql_compose_mutations\GraphQL\Response;
+
+use Drupal\graphql\GraphQL\Response\Response;
+
+/**
+ * Type of response used when a permission is returned.
+ */
+class PermissionsResponse extends Response {
+  /**
+   * The access value to be served.
+   *
+   * @var bool
+   */
+  protected bool $access = FALSE;
+
+  /**
+   * Gets the access.
+   *
+   * @return bool
+   *   The access to be served.
+   */
+  public function getAccess(): bool {
+    return $this->access;
+  }
+
+  /**
+   * Sets the access.
+   *
+   * @param bool $access
+   *   The access value.
+   */
+  public function setAccess(bool $access): void {
+    $this->access = $access;
+  }
+
+  /**
+   * Sets the success value.
+   */
+  public function getSuccess(): bool {
+    return $this->access === TRUE;
+  }
+
+}
diff --git a/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
new file mode 100644
index 00000000..537c7bad
--- /dev/null
+++ b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
@@ -0,0 +1,436 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\graphql_compose_mutations\Plugin\GraphQL\DataProducer;
+
+use Drupal\Component\Utility\Xss;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityStorageException;
+use Drupal\Core\Entity\EntityTypeBundleInfo;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Logger\LoggerChannelFactory;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\graphql\GraphQL\Execution\FieldContext;
+use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
+use Drupal\graphql_compose_mutations\GraphQL\Response\GenericEntityResponse;
+use Drupal\graphql_compose_mutations\Services\UserPermissions;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * This is a generic mutation class used by the SchemaExtension.
+ *
+ * @DataProducer(
+ *   id = "generic_mutation_producer",
+ *   name = @Translation("Mutation"),
+ *   description = @Translation("Generic entity mutation extension."),
+ *   produces = @ContextDefinition("any",
+ *     label = @Translation("Any"),
+ *   ),
+ *   consumes = {
+ *     "data" = @ContextDefinition("any",
+ *       label = @Translation("Entity related data"),
+ *     ),
+ *     "metadata" = @ContextDefinition("any",
+ *       label = @Translation("Generic mutation metadata"),
+ *     ),
+ *   },
+ * )
+ */
+class GenericMutationProducer extends DataProducerPluginBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected AccountInterface $currentUser;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected EntityTypeManagerInterface $entityTypeManager;
+
+  /**
+   * The entity type bundle info.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfo
+   */
+  protected EntityTypeBundleInfo $entityTypeBundleInfo;
+
+  /**
+   * The entity field manager.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected EntityFieldManagerInterface $entityFieldManager;
+
+  /**
+   * The Drupal logger factory service.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelFactory
+   */
+  protected LoggerChannelFactory $loggerChannelFactory;
+
+  /**
+   * The current module user_permissions service.
+   *
+   * @var \Drupal\graphql_compose_mutations\Services\UserPermissions
+   */
+  protected UserPermissions $userPermissions;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    $instance = new self(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+    );
+
+    $instance->currentUser = $container->get('current_user');
+    $instance->entityTypeManager = $container->get('entity_type.manager');
+    $instance->entityTypeBundleInfo = $container->get('entity_type.bundle.info');
+    $instance->entityFieldManager = $container->get('entity_field.manager');
+    $instance->loggerChannelFactory = $container->get('logger.factory');
+    $instance->userPermissions = $container->get('graphql_compose_mutations.user_permissions');
+
+    return $instance;
+  }
+
+  /**
+   * A reusable generic mutation.
+   *
+   * @param array $data
+   *   The mutation data.
+   * @param array $metadata
+   *   Several, entity related metadata required to create the mutation.
+   * @param \Drupal\graphql\GraphQL\Execution\FieldContext $field_context
+   *   The cache context.
+   *
+   * @return \Drupal\graphql_compose_mutations\GraphQL\Response\GenericEntityResponse|null
+   *   The updated or new Entity or a GraphQL response.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+   * @throws \Drupal\Core\Entity\EntityStorageException
+   */
+  public function resolve(array $data, array $metadata, FieldContext $field_context): ?GenericEntityResponse {
+    $response = new GenericEntityResponse();
+
+    // node, group, message, profile etc.
+    // @todo Limit allowed types from graphql_compose settings
+    // This is already done on .graphqls files.
+    $entity_type = strtolower($metadata["entity_type"]);
+    $entity_bundle = strtolower($metadata["entity_bundle"]);
+    $parent_entity_type = isset($metadata["parent_entity_type"]) ? strtolower($metadata["parent_entity_type"]) : NULL;
+    $parent_entity_id = isset($metadata["parent_entity_id"]) ? (int) $metadata["parent_entity_id"] : NULL;
+    $operation = strtolower($metadata["operation"]);
+    if ($operation === "default") {
+      $operation = "create";
+    }
+
+    $id = isset($metadata["id"]) ? (int) $metadata["id"] : NULL;
+    // Default user. We can override this on the $data input argument.
+    $user = $this->currentUser;
+    $uid = $user->id();
+    $entity = NULL;
+    $custom_uid = NULL;
+    $response->setSuccess(FALSE);
+    $final_values = [];
+    // @todo Check if there are other owner like fields on custom entity types.
+    $user_ids = [
+      "uid",
+      "user_id",
+    ];
+
+    // Add default uid to the data array and the metadata.
+    if (in_array($operation, ["update", "delete"]) && $entity_type === "user" && !$id) {
+      $id = $uid;
+    }
+
+    // Protect some entities from being deleted.
+    if ($operation === "delete" && in_array($entity_type, ["user", "profile"]) && (int) $id === 1) {
+      $protected_user = $this->t('User with uid @uid is protected', [
+        "@uid" => $uid,
+      ]);
+      $response->addViolation($protected_user);
+      return $response;
+    }
+
+    // Get parent entity (if we need it. E.g. for the votingapi entities).
+    if ($parent_entity_type && $parent_entity_id) {
+      $parent_entity = $this->entityTypeManager->getStorage($parent_entity_type)->load($parent_entity_id);
+      if (!$parent_entity) {
+        $missing_parent_entity = $this->t('Parent entity with id: @id and type: @type does not exist', [
+          "@id" => $parent_entity_id,
+          "@type" => $parent_entity_type,
+        ]);
+        $response->addViolation($missing_parent_entity);
+        return $response;
+      }
+
+      // For some Mutations we can get some fields from metadata.
+      if ($entity_type === "vote") {
+        if (!isset($data["entity_type"]) && !isset($data["entity_id"])) {
+          $data["entity_type"] = $parent_entity_type;
+          $data["entity_id"] = $parent_entity_id;
+        }
+
+        if ($operation === "create" && !isset($data["value"])) {
+          $data["value"] = 1;
+        }
+      }
+    }
+
+    // Get custom uid from data values.
+    foreach ($user_ids as $user_id_field) {
+      foreach ($data as $field => $value) {
+        if ($field === $user_id_field) {
+          $custom_uid = (int) $value;
+        }
+      }
+    }
+
+    if ($custom_uid && $custom_uid != $uid) {
+      $uid = $custom_uid;
+
+      // Load another User.
+      $user = $this->entityTypeManager
+        ->getStorage("user")
+        ->load($uid);
+
+      if (!$user) {
+        $missing_user = $this->t('User with uid @uid does not exist', [
+          "@uid" => $uid,
+        ]);
+        $response->addViolation($missing_user);
+        return $response;
+      }
+    }
+
+    $revision_log = $this->t("GraphQL entry. Operation: @operation, Type: @entity_type, Bundle: @entity_bundle, Uid: @uid", [
+      '@operation' => $operation,
+      '@entity_type' => $entity_type,
+      '@entity_bundle' => $entity_bundle,
+      '@uid' => $uid,
+    ]);
+    $final_values["revision_log"] = $revision_log;
+
+    // Check entity bundle is valid.
+    $entity_bundles = array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type));
+
+    if (!$entity_bundle or !in_array($entity_bundle, $entity_bundles)) {
+      $entity_bundles_error = $this->t('Entity bundle is required or is not correct. Please use one of @entity_bundles', [
+        "@entity_bundles" => implode(", ", $entity_bundles),
+      ]);
+      $response->addViolation($entity_bundles_error);
+      return $response;
+    }
+
+    // Check permissions early. We use a Service for that.
+    // This also checks if there is an existing entity.
+    $access = $this->userPermissions->userCanDoActionOnEntityByType(
+      $operation,
+      $user->id(),
+      $id,
+      $entity_type,
+      $entity_bundle,
+      $parent_entity_type,
+      $parent_entity_id,
+    );
+
+    if ($access["access"] === FALSE) {
+      $access_error = $access["error"] ?? $this->t('GenericMutationProducer: You do not have permission to perform this operation.');
+      $response->addViolation($access_error);
+      return $response;
+    }
+
+    if ($operation !== "create") {
+      $entity = $this->entityTypeManager
+        ->getStorage($entity_type)
+        ->load($id);
+    }
+
+    // @todo Should we add the line below? Or we need an object as parameter?
+    //   $field_context->addCacheableDependency($access);
+    // Delete operation (does not need values) but may have errors.
+    if ($operation === "delete") {
+      try {
+        $delete_status = $entity->delete();
+      }
+      catch (\Exception $e) {
+        $message = $e->getMessage();
+        $response->addViolation($message);
+        return $response;
+      }
+
+      if ($delete_status instanceof EntityStorageException) {
+        $delete_error = $delete_status->getMessage();
+        $response->addViolation($delete_error);
+      }
+      else {
+        $response->setSuccess(TRUE);
+      }
+
+      $response->setGenericEntity(NULL);
+      return $response;
+    }
+
+    // For non delete operations check additional values exist on input.
+    if (empty($data)) {
+      // If we have no entity values throw an error.
+      $missing_data = $this->t('Missing data values.');
+      $response->addViolation($missing_data);
+      return $response;
+    }
+
+    // Final fields mapping
+    // Get all the entity fields for this bundle.
+    $entity_fields = [];
+    $read_only_fields_to_keep = [
+      "system_entity_is_from_api",
+    ];
+    $entity_fields_all = $this->entityFieldManager->getFieldDefinitions($entity_type, $entity_bundle);
+    // Remove computed fields from graphql_compose_* modules.
+    foreach ($entity_fields_all as $field_name => $field) {
+      $field_array = $field->toArray();
+      $provider = $field_array["provider"] ?? "";
+      $is_computed = $field_array["computed"] ?? FALSE;
+      $is_graphql_compose = $provider && str_starts_with($provider, "graphql_compose");
+      if (!($is_computed && $is_graphql_compose)) {
+        $entity_fields[$field_name] = $field;
+      }
+      // Exceptions for some special read only fields.
+      if (in_array($field_name, $read_only_fields_to_keep)) {
+        $entity_fields[$field_name] = $field;
+      }
+    }
+    $entity_fields_keys = array_keys($entity_fields);
+
+    // Check if $data contains "bad" fields and throw an error.
+    foreach ($data as $field => $value) {
+      if (!in_array($field, $entity_fields_keys)) {
+        $bad_field = $this->t('Field "@field" is not valid key for data.', [
+          "@field" => $field,
+        ]);
+        $response->addViolation($bad_field);
+        return $response;
+      }
+    }
+
+    // @todo Validate complex or composite values (e.g. entity_reference)
+    // @todo Find a way to save the mapping to a config yml
+    // E.g. the body field needs ["value" => "", "format" => "basic_html"].
+    // @codingStandards
+    foreach ($entity_fields_keys as $machine_name) {
+      // Do not process computed fields.
+      foreach ($data as $field => $value) {
+        if ($field === $machine_name) {
+          if (!is_array($value)) {
+            $value = Xss::filter($value);
+          }
+          $final_values[$machine_name] = $value;
+        }
+      }
+    }
+    // Special treatment with the uid. Some entity types have another field.
+    if (in_array("uid", $entity_fields_keys) && $operation === "create") {
+      $final_values["uid"] = $uid;
+    }
+    if (in_array("user_id", $entity_fields_keys)) {
+      $final_values["user_id"] = $uid;
+    }
+    else {
+      unset($final_values["user_id"]);
+    }
+
+    // Clean up manual added values.
+    if (!isset($entity_fields["revision_log"])) {
+      unset($final_values["revision_log"]);
+    }
+
+    // Operation create.
+    if ($operation === "create") {
+
+      // Some entity types do not use "type" as the bundle machine name.
+      // We need to get this through the entityTypeManager.
+      $storage = $this->entityTypeManager->getStorage($entity_type);
+      $type_keys = $storage->getEntityType()->getKeys();
+      $type_machine_name = $type_keys["bundle"] ?? "";
+
+      // Finally, just create the new entity.
+      $entity = $storage->create([$type_machine_name => $entity_bundle]);
+    }
+
+    // Add extra values and save the entity.
+    foreach ($final_values as $machine_name => $value) {
+      try {
+        if (method_exists($entity, 'set')) {
+          $entity->set($machine_name, $value);
+        }
+      }
+      catch (\Exception $e) {
+        $message = $e->getMessage();
+        $response->addViolation($message);
+        return $response;
+      }
+    }
+
+    // @todo Validate entity (this needs the dedicated Entity class usage)
+    try {
+      $save_status = $entity->save();
+    }
+    catch (\Exception $e) {
+      $message = $e->getMessage();
+      $response->addViolation($message);
+      return $response;
+    }
+
+    // If an error occurred (wrong values etc) we do not get Int values.
+    // Type "private_message_thread" returns NULL on UPDATE action.
+    // @todo May need to provide a patch on the module private_message itself.
+    if ($entity_type === "private_message_thread") {
+      if ($operation !== "create") {
+        $entity = $this->entityTypeManager
+          ->getStorage($entity_type)
+          ->load($id);
+        if (!$entity) {
+          $save_error = "Error on entity Save for " . $revision_log;
+          $this->loggerChannelFactory->get("graphql_compose_mutations")
+            ->error($save_error);
+          $response->addViolation($save_error);
+          return $response;
+        }
+      }
+      else {
+        if ($entity->id()) {
+          // Manual set $save_status to 1 for the new private_message_thread.
+          $save_status = 1;
+        }
+      }
+    }
+    elseif ($save_status !== 1 && $save_status !== 2) {
+      // 1: SAVED_NEW, 2: SAVED_UPDATED.
+      $save_error = "Wrong Save status number: " . $save_status . " on entity Save for " . $revision_log;
+      $this->loggerChannelFactory->get("graphql_compose_mutations")->error($save_error);
+      $response->addViolation($save_error);
+      return $response;
+    }
+
+    // @todo Should we add cache context for the entity?
+    //   $field_context->addCacheableDependency($entity);
+    // Pass results to the response.
+    $response->setGenericEntity($entity);
+    if (empty($response->getViolations())) {
+      $response->setSuccess(TRUE);
+    }
+
+    return $response;
+  }
+
+}
diff --git a/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/OperationsByEntityTypeProducer.php b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/OperationsByEntityTypeProducer.php
new file mode 100644
index 00000000..06ee386e
--- /dev/null
+++ b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/OperationsByEntityTypeProducer.php
@@ -0,0 +1,157 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\graphql_compose_mutations\Plugin\GraphQL\DataProducer;
+
+use Drupal\Core\Entity\EntityTypeBundleInfo;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
+use Drupal\graphql_compose_mutations\GraphQL\Response\OperationsByEntityTypeResponse;
+use Drupal\graphql_compose_mutations\Services\UserPermissions;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * This is a helper class that returns the allowed operations by entity type.
+ *
+ * @DataProducer(
+ *   id = "operations_by_entity_type_producer",
+ *   name = @Translation("Operations by Entity Type"),
+ *   description = @Translation("a helper class that returns allowed operations by entity type."),
+ *   produces = @ContextDefinition("any",
+ *     label = @Translation("Any"),
+ *   ),
+ *   consumes = {
+ *     "entity_type" = @ContextDefinition("any",
+ *       label = @Translation("Entity type"),
+ *     ),
+ *   },
+ * )
+ */
+class OperationsByEntityTypeProducer extends DataProducerPluginBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected AccountInterface $currentUser;
+
+  /**
+   * The entity type bundle info.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfo
+   */
+  protected EntityTypeBundleInfo $entityTypeBundleInfo;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected EntityTypeManagerInterface $entityTypeManager;
+
+  /**
+   * The current module user_permissions service.
+   *
+   * @var \Drupal\graphql_compose_mutations\Services\UserPermissions
+   */
+  protected UserPermissions $userPermissions;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    $instance = new self(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+    );
+
+    $instance->currentUser = $container->get('current_user');
+    $instance->entityTypeBundleInfo = $container->get('entity_type.bundle.info');
+    $instance->entityTypeManager = $container->get('entity_type.manager');
+    $instance->userPermissions = $container->get('graphql_compose_mutations.user_permissions');
+
+    return $instance;
+  }
+
+  /**
+   * A query to return allowed operations by entity type.
+   *
+   * @param string $entity_type
+   *   The entity_type.
+   *
+   * @return \Drupal\graphql_compose_mutations\GraphQL\Response\OperationsByEntityTypeResponse|null
+   *   The OperationsByEntityType response.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   */
+  public function resolve(string $entity_type): ?OperationsByEntityTypeResponse {
+    $response = new OperationsByEntityTypeResponse();
+    $results = [];
+    $user = $this->currentUser;
+    $uid = $user->id();
+    $entity_type = strtolower($entity_type);
+    // Some entity types do not have bundles. So they have the type as bundle.
+    $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type) ?? [$entity_type];
+    // @todo add more operations if needed.
+    $operations = [
+      "create",
+    ];
+
+    if (!$entity_type) {
+      $entity_type_error = $this->t('Entity type is required.');
+      $response->addViolation($entity_type_error);
+      return $response;
+    }
+
+    foreach ($bundles as $bundle_key => $bundle_values) {
+      $bundle_entity_type = $this->entityTypeManager->getDefinition($entity_type)->getBundleEntityType();
+      $bundle_description = "";
+      $bundle_label = $bundle_key;
+
+      if ($bundle_entity_type) {
+        $bundle_definition = $this->entityTypeManager
+          ->getStorage($bundle_entity_type)
+          ->load($bundle_key);
+        $bundle_description = $bundle_definition->get("description");
+        $bundle_label = $bundle_definition->label();
+      }
+
+      foreach ($operations as $operation) {
+        $label = $operation . " " . $bundle_label;
+        if ($operation === "create") {
+          $label = "New " . $bundle_label;
+        }
+        // Important. The "create" operation does not need an entity_id.
+        // But we should make it more generic for other operations.
+        // In same cases it needs the Parent entity.
+        $access = $this->userPermissions->userCanDoActionOnEntityByType(
+          $operation,
+          $uid,
+          "",
+          $entity_type,
+          (string) $bundle_key,
+          "",
+          ""
+        );
+        $results[$entity_type][$bundle_key][$operation] = [
+          "access" => $access["access"],
+          "label" => $label,
+          "description" => $bundle_description,
+        ];
+      }
+    }
+
+    if (empty($response->getViolations())) {
+      $response->setOperations($results);
+    }
+
+    return $response;
+  }
+
+}
diff --git a/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/PermissionsProducer.php b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/PermissionsProducer.php
new file mode 100644
index 00000000..8aa4059d
--- /dev/null
+++ b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/PermissionsProducer.php
@@ -0,0 +1,174 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\graphql_compose_mutations\Plugin\GraphQL\DataProducer;
+
+use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
+use Drupal\Component\Plugin\Exception\PluginNotFoundException;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Logger\LoggerChannelFactory;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
+use Drupal\graphql_compose_mutations\GraphQL\Response\PermissionsResponse;
+use Drupal\graphql_compose_mutations\Services\UserPermissions;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * This is a helper class that checks for User operations.
+ *
+ * @DataProducer(
+ *   id = "permissions_producer",
+ *   name = @Translation("Permissions"),
+ *   description = @Translation("a helper class that checks for user operations."),
+ *   produces = @ContextDefinition("any",
+ *     label = @Translation("Any"),
+ *   ),
+ *   consumes = {
+ *     "operation" = @ContextDefinition("any",
+ *       label = @Translation("Operation"),
+ *     ),
+ *     "uid" = @ContextDefinition("any",
+ *       label = @Translation("The User uid"),
+ *     ),
+ *     "entity_id" = @ContextDefinition("any",
+ *       label = @Translation("Entity ID"),
+ *       required = FALSE,
+ *     ),
+ *     "entity_type" = @ContextDefinition("any",
+ *       label = @Translation("Entity type"),
+ *     ),
+ *     "entity_bundle" = @ContextDefinition("any",
+ *       label = @Translation("Entity bundle"),
+ *     ),
+ *     "parent_entity_type" = @ContextDefinition("any",
+ *       label = @Translation("Parent Entity type (optional)"),
+ *       default_value = "",
+ *       required = FALSE,
+ *     ),
+ *     "parent_entity_id" = @ContextDefinition("any",
+ *       label = @Translation("Parent Entity ID (optional)"),
+ *       default_value = "",
+ *       required = FALSE,
+ *     ),
+ *   },
+ * )
+ */
+class PermissionsProducer extends DataProducerPluginBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The current module user_permissions service.
+   *
+   * @var \Drupal\graphql_compose_mutations\Services\UserPermissions
+   */
+  protected UserPermissions $userPermissions;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected EntityTypeManagerInterface $entityTypeManager;
+
+  /**
+   * The Drupal logger factory service.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelFactory
+   */
+  protected LoggerChannelFactory $loggerChannelFactory;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    $instance = new self(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+    );
+
+    $instance->userPermissions = $container->get('graphql_compose_mutations.user_permissions');
+    $instance->entityTypeManager = $container->get('entity_type.manager');
+    $instance->loggerChannelFactory = $container->get('logger.factory');
+
+    return $instance;
+  }
+
+  /**
+   * A query to check for operation permissions by uid.
+   *
+   * @return \Drupal\graphql_compose_mutations\GraphQL\Response\PermissionsResponse|null
+   *   The access Permissions response.
+   */
+  public function resolve(
+    string $operation,
+    mixed $uid,
+    mixed $entity_id,
+    string $entity_type,
+    string $entity_bundle,
+    mixed $parent_entity_type,
+    mixed $parent_entity_id,
+  ): ?PermissionsResponse {
+    $response = new PermissionsResponse();
+    $response->setAccess(FALSE);
+
+    $entity_type = strtolower($entity_type);
+    $entity_bundle = strtolower($entity_bundle);
+    $operation = strtolower($operation);
+
+    $parent_entity_type = $parent_entity_type ? strtolower($parent_entity_type) : NULL;
+    $parent_entity_id = $parent_entity_id ? (int) $parent_entity_id : NULL;
+
+    if ($parent_entity_type && $parent_entity_id) {
+      try {
+        $parent = $this->entityTypeManager->getStorage($parent_entity_type)
+          ->load($parent_entity_id);
+        if (!$parent) {
+          $missing_parent_entity = $this->t('Parent entity with id @uid and type @type does not exist', [
+            "@id" => $parent_entity_id,
+            "@type" => $parent_entity_type,
+          ]);
+          $response->addViolation($missing_parent_entity);
+          return $response;
+        }
+      }
+      catch (InvalidPluginDefinitionException | PluginNotFoundException $e) {
+        $message = $e->getMessage();
+        $this->loggerChannelFactory->get("graphql_compose_mutations")->error($message);
+        $response->addViolation($message);
+        return $response;
+      }
+    }
+
+    // Get access array from Service.
+    try {
+      $access = $this->userPermissions->userCanDoActionOnEntityByType(
+        $operation,
+        $uid,
+        $entity_id,
+        $entity_type,
+        $entity_bundle,
+        $parent_entity_type,
+        $parent_entity_id,
+      );
+
+      // Disallowed.
+      if ($access["access"] === FALSE && isset($access["error"])) {
+        $access_error = $access["error"];
+        $response->addViolation($access_error);
+        return $response;
+      }
+
+      $response->setAccess($access["access"]);
+    }
+    catch (InvalidPluginDefinitionException | PluginNotFoundException $e) {
+      $message = $e->getMessage();
+      $this->loggerChannelFactory->get("graphql_compose_mutations")->error($message);
+      $response->addViolation($message);
+      return $response;
+    }
+
+    return $response;
+  }
+
+}
diff --git a/modules/graphql_compose_mutations/src/Plugin/GraphQL/SchemaExtension/GenericMutationExtension.php b/modules/graphql_compose_mutations/src/Plugin/GraphQL/SchemaExtension/GenericMutationExtension.php
new file mode 100644
index 00000000..91c0c19b
--- /dev/null
+++ b/modules/graphql_compose_mutations/src/Plugin/GraphQL/SchemaExtension/GenericMutationExtension.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\graphql_compose_mutations\Plugin\GraphQL\SchemaExtension;
+
+use Drupal\graphql\GraphQL\ResolverBuilder;
+use Drupal\graphql\GraphQL\ResolverRegistryInterface;
+use Drupal\graphql\Plugin\GraphQL\SchemaExtension\SdlSchemaExtensionPluginBase;
+use Drupal\graphql_compose_mutations\GraphQL\Response\GenericEntityResponse;
+
+/**
+ * Generic Mutation schema.
+ *
+ * @SchemaExtension(
+ *   id = "generic_mutation",
+ *   name = "A generic mutation extension",
+ *   description = "A simple extension to mutate any entity.",
+ *   schema = "graphql_compose"
+ * )
+ */
+class GenericMutationExtension extends SdlSchemaExtensionPluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function registerResolvers(ResolverRegistryInterface $registry): void {
+    $builder = new ResolverBuilder();
+
+    $registry->addFieldResolver('GenericEntityResponse', 'errors',
+      $builder->callback(function (GenericEntityResponse $response) {
+        return $response->getViolations();
+      })
+    );
+
+    $registry->addFieldResolver('GenericEntityResponse', 'success',
+      $builder->callback(function (GenericEntityResponse $response) {
+        return $response->getSuccess();
+      })
+    );
+
+    $registry->addFieldResolver('GenericEntityResponse', 'entity',
+      $builder->callback(function (GenericEntityResponse $response) {
+        return $response->getGenericEntity();
+      })
+    );
+
+    $registry->addFieldResolver(
+      'Mutation',
+      'genericMutation',
+      $builder->produce('generic_mutation_producer')
+        ->map('data', $builder->fromArgument('data'))
+        ->map('metadata', $builder->fromArgument('metadata'))
+    );
+
+  }
+
+}
diff --git a/modules/graphql_compose_mutations/src/Plugin/GraphQL/SchemaExtension/OperationsByEntityTypeExtension.php b/modules/graphql_compose_mutations/src/Plugin/GraphQL/SchemaExtension/OperationsByEntityTypeExtension.php
new file mode 100644
index 00000000..376b95d5
--- /dev/null
+++ b/modules/graphql_compose_mutations/src/Plugin/GraphQL/SchemaExtension/OperationsByEntityTypeExtension.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\graphql_compose_mutations\Plugin\GraphQL\SchemaExtension;
+
+use Drupal\graphql\GraphQL\ResolverBuilder;
+use Drupal\graphql\GraphQL\ResolverRegistryInterface;
+use Drupal\graphql\Plugin\GraphQL\SchemaExtension\SdlSchemaExtensionPluginBase;
+use Drupal\graphql_compose_mutations\GraphQL\Response\OperationsByEntityTypeResponse;
+
+/**
+ * Generic Mutation schema.
+ *
+ * @SchemaExtension(
+ *   id = "operations_by_entity_type",
+ *   name = "Get operations by entity type",
+ *   description = "A query to get operations by entity type extension.",
+ *   schema = "graphql_compose"
+ * )
+ */
+class OperationsByEntityTypeExtension extends SdlSchemaExtensionPluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function registerResolvers(ResolverRegistryInterface $registry): void {
+    $builder = new ResolverBuilder();
+
+    $registry->addFieldResolver('OperationsByEntityTypeResponse', 'errors',
+      $builder->callback(function (OperationsByEntityTypeResponse $response) {
+        return $response->getViolations();
+      })
+    );
+
+    $registry->addFieldResolver('OperationsByEntityTypeResponse', 'operations',
+      $builder->callback(function (OperationsByEntityTypeResponse $response) {
+        return $response->getOperations();
+      })
+    );
+
+    $registry->addFieldResolver(
+      'Query',
+      'operationsByEntityType',
+      $builder->produce('operations_by_entity_type_producer')
+        ->map('entity_type', $builder->fromArgument('entity_type'))
+        ->map('parent_entity_type', $builder->fromArgument('parent_entity_type'))
+        ->map('parent_entity_id', $builder->fromArgument('parent_entity_id'))
+    );
+  }
+
+}
diff --git a/modules/graphql_compose_mutations/src/Plugin/GraphQL/SchemaExtension/PermissionsExtension.php b/modules/graphql_compose_mutations/src/Plugin/GraphQL/SchemaExtension/PermissionsExtension.php
new file mode 100644
index 00000000..54feb403
--- /dev/null
+++ b/modules/graphql_compose_mutations/src/Plugin/GraphQL/SchemaExtension/PermissionsExtension.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\graphql_compose_mutations\Plugin\GraphQL\SchemaExtension;
+
+use Drupal\graphql\GraphQL\ResolverBuilder;
+use Drupal\graphql\GraphQL\ResolverRegistryInterface;
+use Drupal\graphql\Plugin\GraphQL\SchemaExtension\SdlSchemaExtensionPluginBase;
+use Drupal\graphql_compose_mutations\GraphQL\Response\PermissionsResponse;
+
+/**
+ * Permissions Query schema.
+ *
+ * @SchemaExtension(
+ *   id = "permissions",
+ *   name = "A permissions query extension",
+ *   description = "A simple extension to get permissions.",
+ *   schema = "graphql_compose"
+ * )
+ */
+class PermissionsExtension extends SdlSchemaExtensionPluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function registerResolvers(ResolverRegistryInterface $registry): void {
+    $builder = new ResolverBuilder();
+
+    $registry->addFieldResolver('PermissionsResponse', 'errors',
+      $builder->callback(function (PermissionsResponse $response) {
+        return $response->getViolations();
+      })
+    );
+
+    $registry->addFieldResolver('PermissionsResponse', 'success',
+      $builder->callback(function (PermissionsResponse $response) {
+        return $response->getSuccess();
+      })
+    );
+
+    $registry->addFieldResolver(
+      'Query',
+      'permissions',
+      $builder->produce('permissions_producer')
+        ->map('operation', $builder->fromArgument('operation'))
+        ->map('uid', $builder->fromArgument('uid'))
+        ->map('entity_id', $builder->fromArgument('entity_id'))
+        ->map('entity_type', $builder->fromArgument('entity_type'))
+        ->map('entity_bundle', $builder->fromArgument('entity_bundle'))
+        ->map('parent_entity_type', $builder->fromArgument('parent_entity_type'))
+        ->map('parent_entity_id', $builder->fromArgument('parent_entity_id'))
+    );
+  }
+
+}
diff --git a/modules/graphql_compose_mutations/src/Services/UserPermissions.php b/modules/graphql_compose_mutations/src/Services/UserPermissions.php
new file mode 100644
index 00000000..db151c96
--- /dev/null
+++ b/modules/graphql_compose_mutations/src/Services/UserPermissions.php
@@ -0,0 +1,686 @@
+<?php
+
+namespace Drupal\graphql_compose_mutations\Services;
+
+use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
+use Drupal\Component\Plugin\Exception\PluginNotFoundException;
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Entity\EntityTypeBundleInfo;
+use Drupal\Core\Entity\EntityTypeManager;
+use Drupal\Core\Extension\ModuleHandler;
+use Drupal\Core\Logger\LoggerChannel;
+use Drupal\Core\Logger\LoggerChannelInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\node\NodeInterface;
+use Drupal\user\UserInterface;
+
+/**
+ * A helper service to get User permissions by operation.
+ */
+class UserPermissions {
+  /**
+   * The User operations allowed to check for.
+   *
+   * @var array
+   *
+   * @todo This is a duplicate of enum OperationExtended at generic_mutation.base.graphqls
+   * @todo Maybe we could use the existing GraphQL data producer "entity_access".
+   * @todo Add more non-core operation verbs: "like, dislike, flag, unflag, enroll, comment, message etc"
+   */
+  public array $operations = [
+    // Core operations.
+    "view",
+    "create",
+    "update",
+    "delete",
+  ];
+
+  /**
+   * The ModuleHandler service.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandler
+   */
+  protected ModuleHandler $moduleHandler;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected AccountInterface $currentUser;
+
+  /**
+   * Drupal\Core\Entity\EntityTypeManager definition.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManager
+   */
+  protected EntityTypeManager $entityTypeManager;
+
+  /**
+   * Drupal\Core\Entity\EntityTypeBundleInfo definition.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfo
+   */
+  protected EntityTypeBundleInfo $entityTypeBundleInfo;
+
+  /**
+   * The logger service.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelInterface
+   */
+  protected LoggerChannelInterface $logger;
+
+  /**
+   * The Service constructor.
+   *
+   * @param \Drupal\Core\Extension\ModuleHandler $moduleHandler
+   *   The ModuleHandler service.
+   * @param \Drupal\Core\Session\AccountInterface $currentUser
+   *   The current user.
+   * @param \Drupal\Core\Entity\EntityTypeManager $entityTypeManager
+   *   The entity_type.manager service.
+   * @param \Drupal\Core\Logger\LoggerChannel $logger
+   *   The logger.channel.graphql service.
+   * @param \Drupal\Core\Entity\EntityTypeBundleInfo $entityTypeBundleInfo
+   *   The entity_type.manager service.
+   */
+  public function __construct(
+    ModuleHandler $moduleHandler,
+    AccountInterface $currentUser,
+    EntityTypeManager $entityTypeManager,
+    LoggerChannel $logger,
+    EntityTypeBundleInfo $entityTypeBundleInfo,
+  ) {
+    // Get required services.
+    $this->moduleHandler = $moduleHandler;
+    $this->currentUser = $currentUser;
+    $this->entityTypeManager = $entityTypeManager;
+    $this->logger = $logger;
+    $this->entityTypeBundleInfo = $entityTypeBundleInfo;
+  }
+
+  /**
+   * Checks if an entity type (e.g. node, taxonomy_term) exists.
+   *
+   * @param string $entity_type
+   *   The entity type to check for.
+   *
+   * @return bool
+   *   Return if entity type exists.
+   */
+  private function entityTypeExists(string $entity_type):bool {
+    $entity_types = $this->entityTypeBundleInfo->getAllBundleInfo();
+    $entity_type_keys = array_keys($entity_types);
+
+    return in_array($entity_type, $entity_type_keys);
+  }
+
+  /**
+   * Checks if an entity bundle (e.g. event, page of type Node) exists.
+   *
+   * @param string $entity_type
+   *   The entity type to check for.
+   * @param string $entity_bundle
+   *   The entity bundle to check for.
+   *
+   * @return bool
+   *   Return if entity bundle of this type exists.
+   */
+  private function entityBundleOfTypeExists(string $entity_type, string $entity_bundle):bool {
+    $entity_bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type);
+    $entity_bundles_keys = array_keys($entity_bundles);
+
+    return in_array($entity_bundle, $entity_bundles_keys);
+  }
+
+  /**
+   * Get generic permissions manually added. This is a helper method.
+   *
+   * @param string $entity_bundle
+   *   The entity bundle to get the generic permissions for.
+   *
+   * @return array
+   *   Array of manually added permissions.
+   */
+  private function getGenericPermissionsByEntityBundle(string $entity_bundle): array {
+    // Unfortunately, modules do not use a pattern for permissions.
+    // We have to generate this list manually...
+    // @todo only add permissions if entity types exist.
+    $permissions = [
+      "comment" => [
+        "administer comments",
+        "post comments",
+        "create " . $entity_bundle . " comment",
+      ],
+      "contact_message" => [
+        "administer contact forms",
+        "access site-wide contact form",
+      ],
+      "message" => [
+        // @todo This is a system entity so there are no other permissions.
+        "administer messages",
+      ],
+      "node" => [
+        "administer nodes",
+        "bypass node access",
+        "create " . $entity_bundle . " content",
+      ],
+      "group" => [
+        "administer group",
+        "bypass create group access",
+        "bypass group access",
+        "manage all groups",
+        "create " . $entity_bundle . " group",
+      ],
+      "private_message" => [
+        "administer private messages",
+        "use private messaging system",
+      ],
+      "profile" => [
+        "administer profile",
+        "create " . $entity_bundle . " profile",
+      ],
+      "taxonomy_term" => [
+        "administer taxonomy",
+        "create terms in " . $entity_bundle,
+      ],
+    ];
+
+    // Module flag.
+    if ($this->moduleHandler->moduleExists('flag')) {
+      $permissions['flagging'] = [
+        "flag " . $entity_bundle,
+        "unflag " . $entity_bundle,
+      ];
+    }
+
+    // Module contact_storage.
+    if ($this->moduleHandler->moduleExists('contact_storage')) {
+      $permissions['contact_storage'] = [
+        "administer contact forms",
+        "access site-wide contact form",
+      ];
+    }
+
+    // Open Social additional conditions.
+    if ($this->moduleHandler->moduleExists('activity_creator')) {
+      $permissions['activity'] = [
+        "administer activity entities",
+        "add activity entities",
+      ];
+    }
+
+    if ($this->moduleHandler->moduleExists('social_event')) {
+      $permissions['event_enrollment'] = [
+        "administer event enrollment entities",
+        "add event enrollment entities",
+      ];
+    }
+
+    if ($this->moduleHandler->moduleExists('social_private_message')) {
+      $permissions['private_message'][] = "create private messages thread";
+    }
+
+    if ($this->moduleHandler->moduleExists('social_post')) {
+      $permissions['post'] = [
+        "administer post entities",
+        "add post entities",
+        "add " . $entity_bundle . " post entities",
+      ];
+    }
+
+    ksort($permissions);
+
+    return $permissions;
+  }
+
+  /**
+   * Load user from uid. We use the entityTypeManager for dependency injection.
+   *
+   * @param mixed $uid
+   *   The uid.
+   *
+   * @return \Drupal\user\UserInterface|null
+   *   The final User object.
+   */
+  private function loadAccountFromUid(mixed $uid): ?UserInterface {
+    try {
+      $user_storage = $this->entityTypeManager->getStorage("user");
+      $account = $user_storage->load($uid);
+      if ($account instanceof UserInterface) {
+        return $account;
+      }
+    }
+    catch (InvalidPluginDefinitionException | PluginNotFoundException $e) {
+      $this->logger->error($e->getMessage());
+    }
+    return NULL;
+  }
+
+  /**
+   * Check if a user (loaded from uid) has a permission to CREATE.
+   *
+   * @param mixed $uid
+   *   The uid.
+   * @param string $entity_type
+   *   The entity type (node, post, taxonomy_term, etc)
+   * @param string $entity_bundle
+   *   The entity bundle.
+   * @param mixed $parent_entity_type
+   *   The parent entity type that may be related to this operation. E.g. for
+   *   vote entity types this may be the Node or Comment we are voting for.
+   * @param mixed $parent_entity_id
+   *   The parent entity ID. See above.
+   *
+   * @return bool
+   *   Return final access.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+   */
+  public function userCanCreateEntityByType(
+    mixed $uid,
+    string $entity_type,
+    string $entity_bundle,
+    mixed $parent_entity_type,
+    mixed $parent_entity_id,
+  ):bool {
+    $final_access = FALSE;
+    $account = $this->loadAccountFromUid($uid);
+
+    if ($entity_type === "group_content") {
+      if (!$parent_entity_type || !$parent_entity_id) {
+        return FALSE;
+      }
+      $parent_entity = $this->entityTypeManager->getStorage($parent_entity_type)->load($parent_entity_id);
+      // phpcs:disable
+      if ($parent_entity instanceof \Drupal\group\Entity\GroupInterface) {
+        // phpcs:enable
+        /** @var \Drupal\group\Entity\GroupInterface $parent_entity */
+        // If this is a try to CREATE a new Group Membership.
+        if (str_contains($entity_bundle, "-group_membership")) {
+          // @todo Check field allow_request value also.
+          $owner_id = $parent_entity->getOwner()->id();
+          // This is the owner already.
+          if ((int) $owner_id === (int) $uid) {
+            return FALSE;
+          }
+
+          $group_content = $this->entityTypeManager
+            ->getStorage("group_content")
+            ->loadByProperties([
+              "type" => $entity_bundle,
+              "gid" => $parent_entity_id,
+              "entity_id" => $uid,
+            ]);
+
+          // There is already a Group Membership. Skip.
+          if ($group_content) {
+            return FALSE;
+          }
+
+          return TRUE;
+        }
+
+        // @todo What are the Permissions by Group for the other bundles?
+        // @todo Maybe there is a more dedicated Service to handle this.
+        return TRUE;
+      }
+      else {
+        return FALSE;
+      }
+    }
+
+    if ($entity_type === "event_enrollment") {
+      if ($this->moduleHandler->moduleExists('social_event')) {
+        if (!$parent_entity_type || !$parent_entity_id) {
+          return FALSE;
+        }
+        $parent_entity = $this->entityTypeManager->getStorage($parent_entity_type)->load($parent_entity_id);
+        if (!$parent_entity instanceof NodeInterface) {
+          return FALSE;
+        }
+        else {
+          /** @var \Drupal\social_event\Entity\Node\Event $parent_entity */
+          $event_is_enroll_enabled = $parent_entity->isEnrollmentEnabled();
+          if (!$event_is_enroll_enabled) {
+            return FALSE;
+          }
+
+          $event_finished = $this->socialEventHasBeenFinished($parent_entity);
+          if ($event_finished) {
+            return FALSE;
+          }
+
+          // Get enrollment for this Parent entity and this User.
+          $enrollment = $this->entityTypeManager
+            ->getStorage("event_enrollment")
+            ->loadByProperties([
+              "field_event" => $parent_entity_id,
+              "field_account" => $uid,
+            ]);
+          // Already enrolled to the Event.
+          if ($enrollment) {
+            return FALSE;
+          }
+        }
+      }
+    }
+
+    if ($entity_type === "vote") {
+      if (!$parent_entity_type || !$parent_entity_id) {
+        return FALSE;
+      }
+      $parent_entity = $this->entityTypeManager->getStorage($parent_entity_type)->load($parent_entity_id);
+      return like_and_dislike_can_vote($account, $entity_bundle, $parent_entity);
+    }
+
+    if ($entity_type === "flagging") {
+      if (!$parent_entity_type || !$parent_entity_id) {
+        return FALSE;
+      }
+      $parent_entity = $this->entityTypeManager->getStorage($parent_entity_type)->load($parent_entity_id);
+      if (!$parent_entity) {
+        return FALSE;
+      }
+      /** @var \Drupal\Core\Entity\EntityInterface $parent_entity */
+      $flagging_entity = $this->entityTypeManager->getStorage($entity_type)
+        ->loadByProperties([
+          "flag_id" => $entity_bundle,
+          "uid" => $uid,
+          "entity_type" => $parent_entity_type,
+          "entity_id" => $parent_entity_id,
+        ]);
+
+      if ($flagging_entity) {
+        return FALSE;
+      }
+    }
+
+    if ((int) $uid === 1) {
+      return TRUE;
+    }
+
+    $roles = $account->getRoles();
+    foreach ($roles as $role_id) {
+      /** @var \Drupal\user\Entity\Role $role */
+      $role = $this->entityTypeManager->getStorage("user_role")->load($role_id);
+      if ($role->isAdmin()) {
+        return TRUE;
+      }
+    }
+
+    // Core entity access calculations.
+    $access_handler = $this->entityTypeManager->getAccessControlHandler($entity_type);
+    $access_result = (bool) $access_handler->createAccess($entity_bundle, $account);
+    if ($access_result) {
+      return TRUE;
+    }
+
+    // Final, manual added access checks.
+    $create_permissions = $this->getGenericPermissionsByEntityBundle($entity_bundle);
+    if (isset($create_permissions[$entity_type])) {
+      foreach ($create_permissions[$entity_type] as $permission) {
+        $access = $account->hasPermission($permission);
+        if ($access === TRUE) {
+          $final_access = TRUE;
+        }
+      }
+    }
+
+    return $final_access;
+  }
+
+  /**
+   * Check if a user (loaded from uid) has a permission to CREATE.
+   *
+   * @param string $operation
+   *   The operation to check for,.
+   * @param mixed $uid
+   *   The uid.
+   * @param mixed $entity_id
+   *   The entity id to load.
+   * @param string $entity_type
+   *   The entity type (node, post, taxonomy_term etc.)
+   * @param string $entity_bundle
+   *   The entity bundle.
+   * @param mixed $parent_entity_type
+   *   The parent entity type that may be related to this operation. E.g. for
+   *   vote entity types this may be the Node or Comment we are voting for.
+   * @param mixed $parent_entity_id
+   *   The parent entity ID. See above.
+   *
+   * @return array
+   *   Return final access array with useful info.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+   */
+  public function userCanDoActionOnEntityByType(
+    string $operation,
+    mixed $uid,
+    mixed $entity_id,
+    string $entity_type,
+    mixed $entity_bundle,
+    mixed $parent_entity_type,
+    mixed $parent_entity_id,
+  ):array {
+    $current_user = $this->currentUser;
+    $final_access = FALSE;
+    $account = $this->loadAccountFromUid($uid);
+    $operations = $this->operations;
+    $entity_type_exists = $this->entityTypeExists($entity_type);
+    $entity_bundle_exists = $this->entityBundleOfTypeExists($entity_type, $entity_bundle);
+
+    if (!in_array($operation, $operations)) {
+      return [
+        "access" => FALSE,
+        "error" => "userCanDoActionOnEntityByType. Operation is not allowed.",
+      ];
+    }
+
+    if (!$account) {
+      return [
+        "access" => FALSE,
+        "error" => "userCanDoActionOnEntityByType. Account for uid " . $uid . " does not exist.",
+      ];
+    }
+
+    if (!$entity_type_exists) {
+      return [
+        "access" => FALSE,
+        "error" => "userCanDoActionOnEntityByType. Entity type" . $entity_type . " does not exist.",
+      ];
+    }
+
+    if (!$entity_bundle_exists) {
+      return [
+        "access" => FALSE,
+        "error" => "userCanDoActionOnEntityByType. Entity bundle " . $entity_bundle . " of type " . $entity_type . " does not exist.",
+      ];
+    }
+
+    if (in_array($operation, ["update", "delete"]) && $entity_type === "user" && (int) $uid !== (int) $current_user->id()) {
+      return [
+        "access" => FALSE,
+        "error" => "userCanDoActionOnEntityByType. Only owner of the User account can delete-update the account.",
+      ];
+    }
+
+    if ($operation === "create") {
+      $types_with_parent = [
+        "event_enrollment",
+        "flagging",
+        "group_content",
+        "vote",
+      ];
+
+      if (in_array($entity_type, $types_with_parent) && (!$parent_entity_type || !$parent_entity_id)) {
+        return [
+          "access" => FALSE,
+          "error" => "userCanDoActionOnEntityByType. Values for parent_entity_type and parent_entity_id fields are required for type: " . $entity_type,
+        ];
+      }
+
+      $access = $this->userCanCreateEntityByType($uid, $entity_type, $entity_bundle, $parent_entity_type, $parent_entity_id);
+      $result = [
+        "access" => $access,
+      ];
+
+      if (!$access) {
+        $result["error"] = "userCanDoActionOnEntityByType: Access denied for uid " . $uid;
+      }
+      return $result;
+    }
+
+    // Load existing entity.
+    try {
+      $entity = $this->entityTypeManager
+        ->getStorage($entity_type)
+        ->load($entity_id);
+      if ($entity) {
+        if ($entity_type === "group_content") {
+          if ($operation === "delete") {
+            /** @var \Drupal\group\Entity\GroupContent $entity */
+            $gc_id = $entity->get("entity_id")->getString();
+            if ((int) $gc_id === (int) $uid) {
+              return [
+                "access" => TRUE,
+              ];
+            }
+          }
+        }
+
+        if ($entity_type === "flagging") {
+          if ($operation === "delete") {
+            /** @var \Drupal\flag\Entity\Flagging $entity */
+            $flagging_uid = $entity->getOwner()->id();
+            if ((int) $flagging_uid === (int) $uid) {
+              return [
+                "access" => TRUE,
+              ];
+            }
+          }
+        }
+
+        if ($entity_type === "event_enrollment") {
+          if ($operation === "delete") {
+            /** @var \Drupal\social_event\Entity\EventEnrollment $entity */
+            $enrollment_uid = $entity->getOwner()->id();
+            if ((int) $enrollment_uid === (int) $uid) {
+              return [
+                "access" => TRUE,
+              ];
+            }
+          }
+        }
+
+        // Special override for the votingapi module. Check access early.
+        if ($entity_type === "vote") {
+          if (!$parent_entity_type || !$parent_entity_id) {
+            return [
+              "access" => FALSE,
+              "error" => "userCanDoActionOnEntityByType. Parent entity is required for bundle: " . $entity_bundle . " of type: " . $entity_type,
+            ];
+          }
+          $parent_entity = $this->entityTypeManager->getStorage($parent_entity_type)->load($parent_entity_id);
+          if ($parent_entity) {
+            if ($operation === "delete") {
+              /** @var \Drupal\votingapi\Entity\Vote $entity */
+              $vote_uid = $entity->get("user_id")->getString();
+              if ($vote_uid !== $uid) {
+                return [
+                  "access" => FALSE,
+                  "error" => "userCanDoActionOnEntityByType. Vote entity with id: " . $entity_id . " is not from this User.",
+                ];
+              }
+            }
+            $final_access = like_and_dislike_can_vote($account, $entity_bundle, $parent_entity);
+          }
+        }
+
+        // private_message_thread.
+        if ($entity_type === "private_message_thread") {
+          /** @var \Drupal\private_message\Entity\PrivateMessageThread $entity */
+          $members = $entity->getMembersId();
+          if (in_array($current_user->id(), $members)) {
+            $final_access = TRUE;
+          }
+
+          if ($current_user->hasPermission("use private messaging system")) {
+            $final_access = TRUE;
+          }
+        }
+
+        // Initial try to get access and exit with success.
+        if (!$final_access) {
+          $final_access = $entity->access($operation, $account);
+        }
+        if ($final_access === TRUE) {
+          return [
+            "access" => TRUE,
+          ];
+        }
+
+        $result = [
+          "access" => $final_access,
+        ];
+
+        if (!$final_access && !isset($result["error"])) {
+          $result["error"] = "userCanDoActionOnEntityByType. Entity exists. Access denied for uid " . $uid;
+        }
+        return $result;
+      }
+      else {
+        $entity_error = 'userCanDoActionOnEntityByType. Entity of type: ' . $entity_type . ', bundle: ' . $entity_bundle . ' with ID: ' . $entity_id . ' does not exist.';
+        $this->logger->warning($entity_error);
+        return [
+          "access" => FALSE,
+          "error" => $entity_error,
+        ];
+      }
+    }
+    catch (InvalidPluginDefinitionException | PluginNotFoundException $e) {
+      $this->logger->error($e->getMessage());
+      return [
+        "access" => FALSE,
+        "error" => "userCanDoActionOnEntityByType | " . $e->getMessage(),
+      ];
+    }
+  }
+
+  /**
+   * Function to determine if an event has been finished.
+   *
+   * This is a pure copy from Class social_event/src/SocialEventTrait.php
+   * Unfortunately, we can use a private method from a Trait.
+   *
+   * @param \Drupal\node\NodeInterface $node
+   *   The event to check for.
+   *
+   * @return bool
+   *   TRUE if the evens is finished/completed.
+   */
+  public function socialEventHasBeenFinished(NodeInterface $node): bool {
+    $current_time = new DrupalDateTime();
+
+    /** @var \Drupal\Core\Datetime\DrupalDateTime $check_end_date */
+    $check_end_date = $node->get('field_event_date_end')->isEmpty()
+      // @phpstan-ignore-next-line
+      ? $node->get('field_event_date')->date
+      // @phpstan-ignore-next-line
+      : $node->get('field_event_date_end')->date;
+
+    if (!$check_end_date instanceof DrupalDateTime) {
+      return FALSE;
+    }
+
+    $check_all_day = !$node->get('field_event_all_day')->isEmpty()
+      ? $node->get('field_event_all_day')->getString()
+      : NULL;
+
+    return $current_time->getTimestamp() > $check_end_date->getTimestamp() &&
+      !($check_all_day && $check_end_date->format('Y-m-d') === $current_time->format('Y-m-d'));
+  }
+
+}
-- 
GitLab


From fd708f7ff246a73bbbc7c41d446d05e8ccadb020 Mon Sep 17 00:00:00 2001
From: Omar Lopesino <omar.lopesino@metadrop.net>
Date: Wed, 19 Feb 2025 17:19:06 +0100
Subject: [PATCH 02/11] Issue #3458995: Add UUID to the response

---
 .../graphql/generic_mutation.base.graphqls                       | 1 +
 .../src/GraphQL/Response/GenericEntityResponse.php               | 1 +
 2 files changed, 2 insertions(+)

diff --git a/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls b/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls
index f5606373..1f68388b 100644
--- a/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls
+++ b/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls
@@ -78,6 +78,7 @@ A generic object containing any Drupal entity all values
 """
 type AnyEntity {
   id: Int
+  uuid: String
   entity_type: String
   entity_bundle: String
   values: Json
diff --git a/modules/graphql_compose_mutations/src/GraphQL/Response/GenericEntityResponse.php b/modules/graphql_compose_mutations/src/GraphQL/Response/GenericEntityResponse.php
index 3821058c..bfac409f 100644
--- a/modules/graphql_compose_mutations/src/GraphQL/Response/GenericEntityResponse.php
+++ b/modules/graphql_compose_mutations/src/GraphQL/Response/GenericEntityResponse.php
@@ -46,6 +46,7 @@ class GenericEntityResponse extends Response {
     $entity = $this->genericEntity;
     if ($entity) {
       return [
+        "uuid" => $entity->uuid(),
         "values" => $entity->toArray(),
         "id" => $entity->id(),
         "entity_type" => $entity->getEntityTypeId(),
-- 
GitLab


From 11d18da3d2a10a56f4a0a660a62fc7cb638d8e65 Mon Sep 17 00:00:00 2001
From: Omar Lopesino <omar.lopesino@metadrop.net>
Date: Wed, 19 Feb 2025 17:20:03 +0100
Subject: [PATCH 03/11] Issue #3458995: Allow using graphql field names from
 grapqhl compose global settings

---
 .../DataProducer/GenericMutationProducer.php  | 58 ++++++++++++++++---
 1 file changed, 51 insertions(+), 7 deletions(-)

diff --git a/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
index 537c7bad..825ceb68 100644
--- a/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
+++ b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
 namespace Drupal\graphql_compose_mutations\Plugin\GraphQL\DataProducer;
 
 use Drupal\Component\Utility\Xss;
+use Drupal\Core\Config\ImmutableConfig;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Entity\EntityTypeBundleInfo;
@@ -17,6 +18,7 @@ use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
 use Drupal\graphql_compose_mutations\GraphQL\Response\GenericEntityResponse;
 use Drupal\graphql_compose_mutations\Services\UserPermissions;
 use Symfony\Component\DependencyInjection\ContainerInterface;
+use function Symfony\Component\String\u;
 
 /**
  * This is a generic mutation class used by the SchemaExtension.
@@ -82,6 +84,13 @@ class GenericMutationProducer extends DataProducerPluginBase implements Containe
    */
   protected UserPermissions $userPermissions;
 
+  /**
+   * Graphql conpose configuration to perform the fields mapping.
+   *
+   * @var \Drupal\Core\Config\ImmutableConfig
+   */
+  protected ImmutableConfig $graphqlComposeConfiguration;
+
   /**
    * {@inheritdoc}
    */
@@ -98,6 +107,7 @@ class GenericMutationProducer extends DataProducerPluginBase implements Containe
     $instance->entityFieldManager = $container->get('entity_field.manager');
     $instance->loggerChannelFactory = $container->get('logger.factory');
     $instance->userPermissions = $container->get('graphql_compose_mutations.user_permissions');
+    $instance->graphqlComposeConfiguration = $container->get('config.factory')->get('graphql_compose.settings');
 
     return $instance;
   }
@@ -312,9 +322,11 @@ class GenericMutationProducer extends DataProducerPluginBase implements Containe
     }
     $entity_fields_keys = array_keys($entity_fields);
 
+    $fields_mapping = $this->getFieldsMapping($entity_type, $entity_bundle);
+
     // Check if $data contains "bad" fields and throw an error.
     foreach ($data as $field => $value) {
-      if (!in_array($field, $entity_fields_keys)) {
+      if (!in_array($field, $entity_fields_keys) && !in_array($field, $fields_mapping)) {
         $bad_field = $this->t('Field "@field" is not valid key for data.', [
           "@field" => $field,
         ]);
@@ -323,21 +335,24 @@ class GenericMutationProducer extends DataProducerPluginBase implements Containe
       }
     }
 
+
     // @todo Validate complex or composite values (e.g. entity_reference)
     // @todo Find a way to save the mapping to a config yml
     // E.g. the body field needs ["value" => "", "format" => "basic_html"].
     // @codingStandards
     foreach ($entity_fields_keys as $machine_name) {
+      $field_name = $fields_mapping[$machine_name] ?? $machine_name;
+
       // Do not process computed fields.
-      foreach ($data as $field => $value) {
-        if ($field === $machine_name) {
-          if (!is_array($value)) {
-            $value = Xss::filter($value);
-          }
-          $final_values[$machine_name] = $value;
+      if (!empty($data[$field_name])) {
+        $value = $data[$field_name];
+        if (!is_array($value)) {
+          $value = Xss::filter($value);
         }
+        $final_values[$machine_name] = $value;
       }
     }
+
     // Special treatment with the uid. Some entity types have another field.
     if (in_array("uid", $entity_fields_keys) && $operation === "create") {
       $final_values["uid"] = $uid;
@@ -433,4 +448,33 @@ class GenericMutationProducer extends DataProducerPluginBase implements Containe
     return $response;
   }
 
+  /**
+   * Gets the field map between graphql api names and machine names.
+   *
+   * @param string $entity_type
+   *   Entity type.
+   * @param string $entity_bundle
+   *   Entity bundle.
+   *
+   * @return array
+   *   They key is the name_sdl, and the value is the machine name.
+   */
+  protected function getFieldsMapping(string $entity_type, string $entity_bundle) {
+    if ($this->graphqlComposeConfiguration->get('entity_config.' . $entity_type . '.' . $entity_bundle . '.enabled')) {
+      $fields_config = $this->graphqlComposeConfiguration->get('field_config.' . $entity_type . '.' . $entity_bundle);
+      $fields_mapping = [];
+      foreach ($fields_config as $field_name => $configuration) {
+        $fields_mapping[$field_name] = !empty($configuration['enabled']) ? $configuration['name_sdl'] ?? u($field_name)
+          ->trimPrefix('field_')
+          ->camel()
+          ->toString() : $field_name;
+      }
+    }
+    else {
+      $fields_mapping = [];
+    }
+
+    return $fields_mapping;
+  }
+
 }
-- 
GitLab


From 70860e239262adfc5e8dcae1b86a896fdd57b441 Mon Sep 17 00:00:00 2001
From: Omar Lopesino <omar.lopesino@metadrop.net>
Date: Mon, 24 Feb 2025 12:52:52 +0100
Subject: [PATCH 04/11] Issue #3458995: Allow extending user permissions class

---
 .../DataProducer/GenericMutationProducer.php  |  5 +-
 .../src/Services/UserPermissions.php          | 37 +----------
 .../src/Services/UserPermissionsInterface.php | 61 +++++++++++++++++++
 3 files changed, 67 insertions(+), 36 deletions(-)
 create mode 100644 modules/graphql_compose_mutations/src/Services/UserPermissionsInterface.php

diff --git a/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
index 825ceb68..5f023328 100644
--- a/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
+++ b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
@@ -17,6 +17,7 @@ use Drupal\graphql\GraphQL\Execution\FieldContext;
 use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
 use Drupal\graphql_compose_mutations\GraphQL\Response\GenericEntityResponse;
 use Drupal\graphql_compose_mutations\Services\UserPermissions;
+use Drupal\graphql_compose_mutations\Services\UserPermissionsInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use function Symfony\Component\String\u;
 
@@ -80,9 +81,9 @@ class GenericMutationProducer extends DataProducerPluginBase implements Containe
   /**
    * The current module user_permissions service.
    *
-   * @var \Drupal\graphql_compose_mutations\Services\UserPermissions
+   * @var \Drupal\graphql_compose_mutations\Services\UserPermissionsInterface
    */
-  protected UserPermissions $userPermissions;
+  protected UserPermissionsInterface $userPermissions;
 
   /**
    * Graphql conpose configuration to perform the fields mapping.
diff --git a/modules/graphql_compose_mutations/src/Services/UserPermissions.php b/modules/graphql_compose_mutations/src/Services/UserPermissions.php
index db151c96..402e228a 100644
--- a/modules/graphql_compose_mutations/src/Services/UserPermissions.php
+++ b/modules/graphql_compose_mutations/src/Services/UserPermissions.php
@@ -17,7 +17,7 @@ use Drupal\user\UserInterface;
 /**
  * A helper service to get User permissions by operation.
  */
-class UserPermissions {
+class UserPermissions implements UserPermissionsInterface {
   /**
    * The User operations allowed to check for.
    *
@@ -432,29 +432,7 @@ class UserPermissions {
   }
 
   /**
-   * Check if a user (loaded from uid) has a permission to CREATE.
-   *
-   * @param string $operation
-   *   The operation to check for,.
-   * @param mixed $uid
-   *   The uid.
-   * @param mixed $entity_id
-   *   The entity id to load.
-   * @param string $entity_type
-   *   The entity type (node, post, taxonomy_term etc.)
-   * @param string $entity_bundle
-   *   The entity bundle.
-   * @param mixed $parent_entity_type
-   *   The parent entity type that may be related to this operation. E.g. for
-   *   vote entity types this may be the Node or Comment we are voting for.
-   * @param mixed $parent_entity_id
-   *   The parent entity ID. See above.
-   *
-   * @return array
-   *   Return final access array with useful info.
-   *
-   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
-   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+   * {@inheritdoc}
    */
   public function userCanDoActionOnEntityByType(
     string $operation,
@@ -650,16 +628,7 @@ class UserPermissions {
   }
 
   /**
-   * Function to determine if an event has been finished.
-   *
-   * This is a pure copy from Class social_event/src/SocialEventTrait.php
-   * Unfortunately, we can use a private method from a Trait.
-   *
-   * @param \Drupal\node\NodeInterface $node
-   *   The event to check for.
-   *
-   * @return bool
-   *   TRUE if the evens is finished/completed.
+   * {@inheritdoc}
    */
   public function socialEventHasBeenFinished(NodeInterface $node): bool {
     $current_time = new DrupalDateTime();
diff --git a/modules/graphql_compose_mutations/src/Services/UserPermissionsInterface.php b/modules/graphql_compose_mutations/src/Services/UserPermissionsInterface.php
new file mode 100644
index 00000000..abebc1d6
--- /dev/null
+++ b/modules/graphql_compose_mutations/src/Services/UserPermissionsInterface.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\graphql_compose_mutations\Services;
+
+use Drupal\node\NodeInterface;
+
+/**
+ * A helper service to get User permissions by operation.
+ */
+interface UserPermissionsInterface {
+
+  /**
+   * Check if a user (loaded from uid) has a permission to CREATE.
+   *
+   * @param string $operation
+   *   The operation to check for,.
+   * @param mixed $uid
+   *   The uid.
+   * @param mixed $entity_id
+   *   The entity id to load.
+   * @param string $entity_type
+   *   The entity type (node, post, taxonomy_term etc.)
+   * @param string $entity_bundle
+   *   The entity bundle.
+   * @param mixed $parent_entity_type
+   *   The parent entity type that may be related to this operation. E.g. for
+   *   vote entity types this may be the Node or Comment we are voting for.
+   * @param mixed $parent_entity_id
+   *   The parent entity ID. See above.
+   *
+   * @return array
+   *   Return final access array with useful info.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+   */
+  public function userCanDoActionOnEntityByType(
+    string $operation,
+    mixed $uid,
+    mixed $entity_id,
+    string $entity_type,
+    mixed $entity_bundle,
+    mixed $parent_entity_type,
+    mixed $parent_entity_id,
+  ) : array;
+
+  /**
+   * Function to determine if an event has been finished.
+   *
+   * This is a pure copy from Class social_event/src/SocialEventTrait.php
+   * Unfortunately, we can use a private method from a Trait.
+   *
+   * @param \Drupal\node\NodeInterface $node
+   *   The event to check for.
+   *
+   * @return bool
+   *   TRUE if the evens is finished/completed.
+   */
+  public function socialEventHasBeenFinished(NodeInterface $node): bool;
+
+}
-- 
GitLab


From 1c905856b31b4799a71e0540b1dc6c57f5007114 Mon Sep 17 00:00:00 2001
From: Omar Lopesino <omar.lopesino@metadrop.net>
Date: Mon, 3 Mar 2025 18:14:42 +0100
Subject: [PATCH 05/11] Issue #3458995: Validate entity

---
 .../DataProducer/GenericMutationProducer.php        | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
index 5f023328..a0e7f318 100644
--- a/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
+++ b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
@@ -6,6 +6,7 @@ namespace Drupal\graphql_compose_mutations\Plugin\GraphQL\DataProducer;
 
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Config\ImmutableConfig;
+use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Entity\EntityTypeBundleInfo;
@@ -397,7 +398,17 @@ class GenericMutationProducer extends DataProducerPluginBase implements Containe
       }
     }
 
-    // @todo Validate entity (this needs the dedicated Entity class usage)
+    // Validate entity.
+    if ($entity instanceof ContentEntityInterface) {
+      $violations = $entity->validate();
+      if ($violations->count() > 0) {
+        foreach ($violations as $violation) {
+          $response->addViolation($violation->getMessage());
+        }
+        return $response;
+      }
+    }
+
     try {
       $save_status = $entity->save();
     }
-- 
GitLab


From 35c08f75b7f2f1ec8dbf66c5749a4bdd561b4255 Mon Sep 17 00:00:00 2001
From: Omar Lopesino <omar.lopesino@metadrop.net>
Date: Wed, 5 Mar 2025 09:28:58 +0100
Subject: [PATCH 06/11] Issue #3458995: Support media

---
 .../graphql/generic_mutation.base.graphqls                       | 1 +
 1 file changed, 1 insertion(+)

diff --git a/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls b/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls
index 1f68388b..e4ab68c4 100644
--- a/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls
+++ b/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls
@@ -62,6 +62,7 @@ enum EntityType {
   FLAGGING
   GROUP
   GROUP_CONTENT
+  MEDIA
   MESSAGE
   NODE
   POST
-- 
GitLab


From 34205f9a781d463c697e8e9956fb2409d65c09f6 Mon Sep 17 00:00:00 2001
From: Omar Lopesino <omar.lopesino@metadrop.net>
Date: Thu, 6 Mar 2025 10:25:13 +0100
Subject: [PATCH 07/11] Issue #3458995: Improve mapping fields on create

Create now adds all the fields in the create method, so we have all
fields available in hook_entity_create
---
 .../DataProducer/GenericMutationProducer.php  | 72 +++++++++++++------
 1 file changed, 49 insertions(+), 23 deletions(-)

diff --git a/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
index a0e7f318..330f3d60 100644
--- a/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
+++ b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
@@ -8,6 +8,7 @@ use Drupal\Component\Utility\Xss;
 use Drupal\Core\Config\ImmutableConfig;
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityStorageException;
 use Drupal\Core\Entity\EntityTypeBundleInfo;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
@@ -371,32 +372,21 @@ class GenericMutationProducer extends DataProducerPluginBase implements Containe
       unset($final_values["revision_log"]);
     }
 
-    // Operation create.
-    if ($operation === "create") {
-
-      // Some entity types do not use "type" as the bundle machine name.
-      // We need to get this through the entityTypeManager.
-      $storage = $this->entityTypeManager->getStorage($entity_type);
-      $type_keys = $storage->getEntityType()->getKeys();
-      $type_machine_name = $type_keys["bundle"] ?? "";
-
-      // Finally, just create the new entity.
-      $entity = $storage->create([$type_machine_name => $entity_bundle]);
-    }
-
-    // Add extra values and save the entity.
-    foreach ($final_values as $machine_name => $value) {
-      try {
-        if (method_exists($entity, 'set')) {
-          $entity->set($machine_name, $value);
-        }
+    try {
+      if ($operation === "create") {
+        $entity = $this->createEntity($entity_type, $entity_bundle, $final_values);
       }
-      catch (\Exception $e) {
-        $message = $e->getMessage();
-        $response->addViolation($message);
-        return $response;
+      else {
+        if ($entity instanceof ContentEntityInterface) {
+          $this->updateEntityValues($entity, $final_values);
+        }
       }
     }
+    catch (\Exception $e) {
+      $message = $e->getMessage();
+      $response->addViolation($message);
+      return $response;
+    }
 
     // Validate entity.
     if ($entity instanceof ContentEntityInterface) {
@@ -460,6 +450,42 @@ class GenericMutationProducer extends DataProducerPluginBase implements Containe
     return $response;
   }
 
+  /**
+   * Create the entity.
+   *
+   * @param string $entity_type
+   *   Entity type.
+   * @param string $entity_bundle
+   *   Entity bundle.
+   * @param array $values
+   *   Values.
+   */
+  protected function createEntity($entity_type, $entity_bundle, $values) {
+
+    // Some entity types do not use "type" as the bundle machine name.
+    // We need to get this through the entityTypeManager.
+    $storage = $this->entityTypeManager->getStorage($entity_type);
+    $type_keys = $storage->getEntityType()->getKeys();
+    $type_machine_name = $type_keys["bundle"] ?? "";
+
+    return $storage->create([$type_machine_name => $entity_bundle] + $values);
+  }
+
+  /**
+   * Update the values of an existing entity.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   Entity.
+   * @param array $values
+   *   Values.
+   */
+  protected function updateEntityValues(ContentEntityInterface $entity, array $values) {
+    // Add extra values and save the entity.
+    foreach ($values as $machine_name => $value) {
+      $entity->set($machine_name, $value);
+    }
+  }
+
   /**
    * Gets the field map between graphql api names and machine names.
    *
-- 
GitLab


From 08b1840b580c510a116c8e6450ca0ad9d4a9fb51 Mon Sep 17 00:00:00 2001
From: Omar Lopesino <omar.lopesino@metadrop.net>
Date: Thu, 6 Mar 2025 10:25:39 +0100
Subject: [PATCH 08/11] Issue #3458995: Support commerce_product

---
 .../graphql/generic_mutation.base.graphqls                       | 1 +
 1 file changed, 1 insertion(+)

diff --git a/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls b/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls
index e4ab68c4..8d4c3fae 100644
--- a/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls
+++ b/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls
@@ -57,6 +57,7 @@ The mutation available entity types
 enum EntityType {
   ACTIVITY
   COMMENT
+  COMMERCE_PRODUCT
   CONTACT_MESSAGE
   EVENT_ENROLLMENT
   FLAGGING
-- 
GitLab


From 65a1aaa23fea1824ddf598a10210fd9b88b7032a Mon Sep 17 00:00:00 2001
From: Omar Lopesino <omar.lopesino@metadrop.net>
Date: Fri, 7 Mar 2025 16:56:55 +0100
Subject: [PATCH 09/11] Issue #3458995: Support commerce products

---
 .../graphql/generic_mutation.base.graphqls                       | 1 +
 1 file changed, 1 insertion(+)

diff --git a/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls b/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls
index 8d4c3fae..7040c48b 100644
--- a/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls
+++ b/modules/graphql_compose_mutations/graphql/generic_mutation.base.graphqls
@@ -57,6 +57,7 @@ The mutation available entity types
 enum EntityType {
   ACTIVITY
   COMMENT
+  COMMERCE_ORDER
   COMMERCE_PRODUCT
   CONTACT_MESSAGE
   EVENT_ENROLLMENT
-- 
GitLab


From eb1089131f35d8e301b0bbb3ae2a7dae0a372326 Mon Sep 17 00:00:00 2001
From: Omar Lopesino <omar.lopesino@metadrop.net>
Date: Thu, 20 Mar 2025 09:19:16 +0100
Subject: [PATCH 10/11] Issue #3458995: Prevent iterate a null configuration

---
 .../src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
index 330f3d60..78338d44 100644
--- a/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
+++ b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
@@ -499,7 +499,7 @@ class GenericMutationProducer extends DataProducerPluginBase implements Containe
    */
   protected function getFieldsMapping(string $entity_type, string $entity_bundle) {
     if ($this->graphqlComposeConfiguration->get('entity_config.' . $entity_type . '.' . $entity_bundle . '.enabled')) {
-      $fields_config = $this->graphqlComposeConfiguration->get('field_config.' . $entity_type . '.' . $entity_bundle);
+      $fields_config = $this->graphqlComposeConfiguration->get('field_config.' . $entity_type . '.' . $entity_bundle) ?? [];
       $fields_mapping = [];
       foreach ($fields_config as $field_name => $configuration) {
         $fields_mapping[$field_name] = !empty($configuration['enabled']) ? $configuration['name_sdl'] ?? u($field_name)
-- 
GitLab


From 41db580dfb81ab028d2db41b3f0ce2349890f011 Mon Sep 17 00:00:00 2001
From: Omar Lopesino <omar.lopesino@metadrop.net>
Date: Thu, 24 Apr 2025 15:58:59 +0200
Subject: [PATCH 11/11] Issue #3458995: Specify which field fails in validation

---
 .../Plugin/GraphQL/DataProducer/GenericMutationProducer.php    | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
index 78338d44..e5be32d8 100644
--- a/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
+++ b/modules/graphql_compose_mutations/src/Plugin/GraphQL/DataProducer/GenericMutationProducer.php
@@ -392,8 +392,9 @@ class GenericMutationProducer extends DataProducerPluginBase implements Containe
     if ($entity instanceof ContentEntityInterface) {
       $violations = $entity->validate();
       if ($violations->count() > 0) {
+        /** @var \Symfony\Component\Validator\ConstraintViolation $violation */
         foreach ($violations as $violation) {
-          $response->addViolation($violation->getMessage());
+          $response->addViolation($violation->getMessage(), ['property' => $violation->getPropertyPath()]);
         }
         return $response;
       }
-- 
GitLab