graphql_compose-1.0.0-beta20/modules/graphql_compose_edges/src/EntityConnection.php

modules/graphql_compose_edges/src/EntityConnection.php
<?php

declare(strict_types=1);

namespace Drupal\graphql_compose_edges;

use Drupal\graphql\GraphQL\Execution\FieldContext;
use Drupal\graphql_compose_edges\Wrappers\Cursor;
use Drupal\graphql_compose_edges\Wrappers\EdgeInterface;
use GraphQL\Error\UserError;
use GraphQL\Executor\Promise\Adapter\SyncPromise;

/**
 * Provides a new paginated entity query.
 */
class EntityConnection implements ConnectionInterface {

  /**
   * The number of nodes a client is allowed to fetch on this connection.
   */
  public const MAX_LIMIT = 100;

  /**
   * Fetch the first N results.
   *
   * @var int|null
   */
  protected ?int $first = NULL;

  /**
   * Fetch the last N results.
   *
   * @var int|null
   */
  protected ?int $last = NULL;

  /**
   * The cursor that results were fetched after.
   *
   * @var string|null
   */
  protected ?string $after = NULL;

  /**
   * The cursor that results were fetched before.
   *
   * @var string|null
   */
  protected ?string $before = NULL;

  /**
   * Whether the sorting is requested in reversed order.
   *
   * @var bool
   */
  protected bool $reverse = FALSE;

  /**
   * The filters to apply to the cursor.
   *
   * @var array
   */
  protected array $filters = [];

  /**
   * The cache context for this request.
   *
   * @var \Drupal\graphql\GraphQL\Execution\FieldContext
   */
  protected FieldContext $context;

  /**
   * The result-set of this connection.
   *
   * @var \GraphQL\Executor\Promise\Adapter\SyncPromise|null
   */
  protected ?SyncPromise $result;

  /**
   * Create a new PaginatedEntityQuery.
   *
   * @param \Drupal\graphql_compose_edges\ConnectionQueryHelperInterface $queryHelper
   *   The query helper that knows how to fetch the data for this connection.
   */
  public function __construct(
    protected ConnectionQueryHelperInterface $queryHelper,
  ) {}

  /**
   * {@inheritdoc}
   */
  public function setCacheContext(FieldContext $context): static {
    $this->context =& $context;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheContext(): FieldContext {
    return $this->context;
  }

  /**
   * {@inheritdoc}
   */
  public function setFilter(string $key, $value, string $filter_class): static {
    $this->filters[$key] = [
      'value' => $value,
      'class' => $filter_class,
    ];

    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getFilter($key) {
    return $this->filters[$key]['value'] ?? NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getFilters(): array {
    return $this->filters;
  }

  /**
   * Get the data result for this connection.
   *
   * @return \GraphQL\Executor\Promise\Adapter\SyncPromise
   *   The result for this connection's query.
   */
  protected function getResult(): SyncPromise {
    return $this->result ??= $this->execute();
  }

  /**
   * {@inheritdoc}
   */
  public function getQueryHelper(): ConnectionQueryHelperInterface {
    return $this->queryHelper;
  }

  /**
   * {@inheritdoc}
   */
  public function edges(): SyncPromise {
    return $this->getResult()->then($this->sliceAndReorder(...));
  }

  /**
   * {@inheritdoc}
   */
  public function nodes(): SyncPromise {
    return $this->edges()->then(function ($edges) {

        // Nodes are a shortcut into the edge.
        // We only need the nodes.
        return array_map(
          fn (EdgeInterface $edge) => $edge->getNode(),
          $edges
        );
    });
  }

  /**
   * Apply slicing and reordering to the result so that it can be transmitted.
   *
   * The result from the database may be out of order and have over-fetched.
   * When returning edges or nodes, this needs to be compensated in the
   * same way. This function removes the over-fetching and ensures the
   * results are in the requested order.
   *
   * @param \Drupal\graphql_compose_edges\Wrappers\EdgeInterface[] $edges
   *   Edges to check access on.
   *
   * @return \Drupal\graphql_compose_edges\Wrappers\EdgeInterface[]
   *   Filtered edge nodes.
   */
  protected function sliceAndReorder(array $edges): array {
    // To allow for pagination we over-fetch results by one above
    // the limits so we must fix that now.
    $edges = array_slice($edges, 0, $this->first ?: $this->last);

    if ($this->shouldReverseResultEdges()) {
      $edges = array_reverse($edges);
    }

    return $edges;
  }

  /**
   * Execute the query to fetch the entities in this connection.
   *
   * @return \GraphQL\Executor\Promise\Adapter\SyncPromise
   *   A promise that resolves to the edges of this connection.
   *
   * @throws \GraphQL\Error\UserError
   *   Invalid cursor provided.
   */
  protected function execute(): SyncPromise {
    $query = $this->queryHelper->getQuery();
    $sort_field = $this->queryHelper->getSortField();
    $id_field = $this->queryHelper->getIdField();

    // Because MySQL only allows us to provide positive range limits (we can't
    // select backwards) we must change the query order based on the meaning of
    // first and last. This in turn is dependant on whether we're selecting in
    // ascending (non-reversed) or descending (reversed) order.
    // The order is ascending if
    // - we want the first results in a non reversed query
    // - we want the last results in a reversed query
    // The order is descending if
    // - we want the first results in a reversed query
    // - we want the last results in a non reversed query.
    $field_query_order = (!is_null($this->first) && !$this->reverse) || (!is_null($this->last) && $this->reverse)
      ? 'ASC'
      : 'DESC';

    // @todo flesh out field sorting.
    $id_query_order = (!is_null($this->first) && !$this->reverse) || (!is_null($this->last) && $this->reverse)
      ? 'ASC'
      : 'DESC';

    // If a cursor is provided then we alter the condition to select the
    // elements on the correct side of the cursor.
    $cursor = $this->after ?: $this->before;

    if (!is_null($cursor)) {
      $cursor_object = Cursor::fromCursorString($cursor);

      // Validate the cursor against the query expected.
      $cursor_valid = $cursor_object->validate(
        $this->queryHelper->getEntityTypeId(),
        $this->queryHelper->getSortKey(),
        $this->filters
      );

      if (!$cursor_valid) {
        throw new UserError(sprintf('Invalid cursor %s', $cursor));
      }

      $operator = (!is_null($this->before) && !$this->reverse) || (!is_null($this->after) && $this->reverse) ? '<' : '>';

      $cursor_id = $cursor_object->getBackingId();
      $cursor_sort = $cursor_object->getSortValue();

      $pagination_condition = $query->orConditionGroup();
      $pagination_condition->condition($sort_field, $cursor_sort, $operator);

      // If the sort field is different than the ID then it's not guaranteed to
      // be unique. However, above we exclude values that are the same as those
      // of the cursor. We want to include those but use the ID to make sure
      // they're on the correct side of the cursor.
      if ($sort_field !== $id_field) {
        $pagination_condition->condition(
          $query->andConditionGroup()
            ->condition($sort_field, $cursor_sort, '=')
            ->condition($id_field, $cursor_id, $operator)
        );
      }

      $query->condition($pagination_condition);
    }

    // From assertValidPagination we know that we either have a first or a last.
    $limit = $this->first ?: $this->last;

    // Fetch N + 1 so we know if there are more pages.
    $query->range(0, $limit + 1);

    // Sort by field and ID to ensure a stable sort.
    $query->sort($sort_field, $field_query_order);
    if ($sort_field !== $id_field) {
      $query->sort($id_field, $id_query_order);
    }

    // Add conditional filters.
    foreach ($this->filters as $filter) {
      $filter_instance = new $filter['class']($this, $cursor_object ?? NULL);
      $filter_instance->apply($query);
    }

    // Fetch the result for the query.
    $result = $query->execute();

    return $this->queryHelper->resolve($this, $result);
  }

  /**
   * Whether the edges from our result should be reversed.
   *
   * To compensate for the ordering needed for the range selector we must
   * sometimes flip the result. The first 3 results of a non-reverse query
   * are the same as the last 3 results of a reversed query but they are in
   * reverse order.
   * The results must be flipped if
   * - we want the last results in a reversed query
   * - we want the last results in a non reversed query.
   *
   * @return bool
   *   Whether the edges returned from `getResult()` as in reverse order.
   */
  protected function shouldReverseResultEdges(): bool {
    return !is_null($this->last);
  }

  /**
   * {@inheritdoc}
   */
  public function pageInfo(): SyncPromise {
    return $this->getResult()->then(function ($edges) {
      /** @var \Drupal\graphql_compose_edges\Wrappers\Edge[] $edges */

      // If we don't have any results then we won't have any other pages either.
      if (empty($edges)) {
        return [
          'hasNextPage' => FALSE,
          'hasPreviousPage' => FALSE,
          'startCursor' => NULL,
          'endCursor' => NULL,
        ];
      }

      // Count the number of elements that we have so we can check if we have
      // future pages.
      $count = count($edges);

      // The last item is either based on the limit or on the number of fetched
      // items if it's below the limit. Correct for 0 based indexing.
      $last_index = min($this->first ?: $this->last, $count) - 1;

      return [
        // We have a next page if the before cursor was provided (we assume
        // calling code has validated the cursor) or if N first results were
        // requested and we have more.
        'hasNextPage' => $this->before !== NULL || ($this->first !== NULL && $this->first < $count),

        // We have a previous page if the after cursor was provided (we assume
        // calling code has validated the cursor) or if N last results were
        // requested and we have more.
        'hasPreviousPage' => $this->after !== NULL || ($this->last !== NULL && $this->last < $count),

        // The start cursor is always the first cursor in the result-set..
        'startCursor' => $this->shouldReverseResultEdges() ? $edges[$last_index]->getCursor() : $edges[0]->getCursor(),

        // The end cursor is always the last cursor in the result-set..
        'endCursor' => $this->shouldReverseResultEdges() ? $edges[0]->getCursor() : $edges[$last_index]->getCursor(),
      ];
    });
  }

  /**
   * {@inheritdoc}
   *
   * @throws \RuntimeException
   */
  public function setPagination(?int $first, ?string $after, ?int $last, ?string $before, ?bool $reverse): static {
    // Disallow changing pagination after a query has been performed
    // because the way we treat the results depends on it.
    if (isset($this->result)) {
      throw new \RuntimeException('Cannot change pagination after a query for a connection has been executed.');
    }

    $this->validatePagination($first, $after, $last, $before);

    $this->first = $first;
    $this->after = $after;
    $this->last = $last;
    $this->before = $before;
    $this->reverse = (bool) $reverse;

    return $this;
  }

  /**
   * Ensures the user entered limits (first/last) are valid.
   *
   * @param int|null $first
   *   Request to retrieve first n results.
   * @param string|null $after
   *   The cursor after which to fetch results.
   * @param int|null $last
   *   Request to retrieve last n results.
   * @param string|null $before
   *   The cursor before which to fetch results.
   *
   * @throws \GraphQL\Error\UserError
   *   Error thrown when a user has specified invalid arguments.
   */
  protected function validatePagination(?int $first, ?string $after, ?int $last, ?string $before): void {

    // The limit on the amount of results that may be requested.
    $limit = $this->queryHelper->getLimit() ?: self::MAX_LIMIT;

    if ($first > $limit) {
      throw new UserError(sprintf('First may not be larger than %s.', $limit));
    }
    if ($last > $limit) {
      throw new UserError(sprintf('Last may not be larger than %s.', $limit));
    }

    // The below if-statements are derived to be able to implement the Relay
    // connection spec in a sane way. They ensure we only ever need to care
    // about either (first and after) or (last and before) and no other
    // combinations.
    if (is_null($first) && is_null($last)) {
      throw new UserError('You must provide one of first or last.');
    }
    if (!is_null($first) && !is_null($last)) {
      throw new UserError('Providing both first and last is not supported.');
    }
    if (!is_null($first) && !is_null($before)) {
      throw new UserError('Using first with before is not supported.');
    }
    if (!is_null($last) && !is_null($after)) {
      throw new UserError('Using last with after is not supported.');
    }
    if ($first <= 0 && !is_null($first)) {
      throw new UserError('First must be a positive integer when provided.');
    }
    if ($last <= 0 && !is_null($last)) {
      throw new UserError('Last must be a positive integer when provided.');
    }
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc