├── .scrutinizer.yml ├── .travis.yml ├── README.md ├── composer.json ├── config └── schema │ └── graphql_views.views.schema.yml ├── graphql_views.info.yml ├── phpcs.xml.dist ├── src ├── Plugin │ ├── Deriver │ │ ├── Enums │ │ │ └── ViewSortByDeriver.php │ │ ├── Fields │ │ │ ├── ViewDeriver.php │ │ │ ├── ViewResultCountDeriver.php │ │ │ ├── ViewResultListDeriver.php │ │ │ └── ViewRowFieldDeriver.php │ │ ├── InputTypes │ │ │ ├── ViewContextualFilterInputDeriver.php │ │ │ └── ViewFilterInputDeriver.php │ │ ├── Types │ │ │ ├── ViewResultTypeDeriver.php │ │ │ └── ViewRowTypeDeriver.php │ │ └── ViewDeriverBase.php │ ├── GraphQL │ │ ├── Enums │ │ │ ├── ViewSortBy.php │ │ │ └── ViewSortDirection.php │ │ ├── Fields │ │ │ ├── Entity │ │ │ │ └── Fields │ │ │ │ │ └── View │ │ │ │ │ └── ViewDerivative.php │ │ │ ├── View.php │ │ │ ├── ViewResultCount.php │ │ │ ├── ViewResultList.php │ │ │ └── ViewRowField.php │ │ ├── InputTypes │ │ │ ├── ViewContextualFilterInput.php │ │ │ └── ViewFilterInput.php │ │ ├── Scalars │ │ │ └── TypedData │ │ │ │ ├── ViewsContextualFilterInput.php │ │ │ │ ├── ViewsFilterInput.php │ │ │ │ └── ViewsSortByInput.php │ │ ├── Types │ │ │ ├── ViewResultType.php │ │ │ └── ViewRowType.php │ │ └── UnionTypes │ │ │ └── ViewResult.php │ └── views │ │ ├── display │ │ └── GraphQL.php │ │ ├── exposed_form │ │ └── GraphQL.php │ │ ├── row │ │ ├── GraphQLEntityRow.php │ │ └── GraphQLFieldRow.php │ │ └── style │ │ └── GraphQL.php └── ViewDeriverHelperTrait.php └── tests ├── modules └── graphql_views_test │ ├── config │ └── install │ │ ├── views.view.graphql_bundle_test.yml │ │ └── views.view.graphql_test.yml │ ├── graphql_views_test.features.yml │ ├── graphql_views_test.info.yml │ └── graphql_views_test.module ├── queries ├── contextual.gql ├── paged.gql ├── simple.gql ├── single_bundle_filter.gql └── sorted.gql └── src └── Kernel ├── ContextualViewsTest.php ├── ViewsTest.php └── ViewsTestBase.php /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: 3 | - 'tests/*' 4 | 5 | checks: 6 | php: true 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | sudo: false 3 | 4 | php: 5 | - 7.3 6 | - 7.2 7 | - 7.1 8 | - 7.0 9 | 10 | services: 11 | - mysql 12 | 13 | env: 14 | global: 15 | - DRUPAL_GRAPHQL=8.x-3.x 16 | - DRUPAL_BUILD_DIR=$TRAVIS_BUILD_DIR/../drupal 17 | - SIMPLETEST_DB=mysql://root:@127.0.0.1/graphql 18 | - TRAVIS=true 19 | matrix: 20 | - DRUPAL_CORE=8.7.x 21 | - DRUPAL_CORE=8.8.x 22 | 23 | matrix: 24 | # Don't wait for the allowed failures to build. 25 | fast_finish: true 26 | include: 27 | - php: 7.3 28 | env: 29 | - DRUPAL_CORE=8.7.x 30 | # Only run code coverage on the latest php and drupal versions. 31 | - WITH_PHPDBG_COVERAGE=true 32 | allow_failures: 33 | # Allow the code coverage report to fail. 34 | - php: 7.3 35 | env: 36 | - DRUPAL_CORE=8.7.x 37 | # Only run code coverage on the latest php and drupal versions. 38 | - WITH_PHPDBG_COVERAGE=true 39 | 40 | mysql: 41 | database: graphql 42 | username: root 43 | encoding: utf8 44 | 45 | # Cache composer downloads. 46 | cache: 47 | directories: 48 | - $HOME/.composer 49 | 50 | before_install: 51 | # Disable xdebug. 52 | - phpenv config-rm xdebug.ini 53 | 54 | # Determine the php settings file location. 55 | - if [[ $TRAVIS_PHP_VERSION = hhvm* ]]; 56 | then export PHPINI=/etc/hhvm/php.ini; 57 | else export PHPINI=$HOME/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; 58 | fi 59 | 60 | # Disable the default memory limit. 61 | - echo memory_limit = -1 >> $PHPINI 62 | 63 | # Update composer. 64 | - composer self-update 65 | 66 | install: 67 | # Create the database. 68 | - mysql -e 'create database graphql' 69 | 70 | # Download Drupal 8 core from the Github mirror because it is faster. 71 | - git clone --branch $DRUPAL_CORE --depth 1 https://github.com/drupal/drupal.git $DRUPAL_BUILD_DIR 72 | - git clone --branch $DRUPAL_GRAPHQL --depth 1 https://github.com/drupal-graphql/graphql.git $DRUPAL_BUILD_DIR/modules/graphql 73 | 74 | # Reference the module in the build site. 75 | - ln -s $TRAVIS_BUILD_DIR $DRUPAL_BUILD_DIR/modules/graphql_views 76 | 77 | # Copy the customized phpunit configuration file to the core directory so 78 | # the relative paths are correct. 79 | - cp $DRUPAL_BUILD_DIR/modules/graphql/phpunit.xml.dist $DRUPAL_BUILD_DIR/core/phpunit.xml 80 | 81 | # When running with phpdbg we need to replace all code occurrences that check 82 | # for 'cli' with 'phpdbg'. Some files might be write protected, hence the 83 | # fallback. 84 | - if [[ "$WITH_PHPDBG_COVERAGE" == "true" ]]; 85 | then grep -rl 'cli' $DRUPAL_BUILD_DIR/core $DRUPAL_BUILD_DIR/modules | xargs sed -i "s/'cli'/'phpdbg'/g" || true; 86 | fi 87 | 88 | # Bring in the module dependencies without requiring a merge plugin. The 89 | # require also triggers a full 'composer install'. 90 | - composer --working-dir=$DRUPAL_BUILD_DIR require webonyx/graphql-php:^0.12.5 91 | 92 | # For Drupal < 8.8 we have to manually upgrade zend-stdlib to avoid PHP 7.3 93 | # incompatibilities. 94 | - if [[ "$DRUPAL_CORE" = "8.6.x" || "$DRUPAL_CORE" = "8.7.x" ]]; 95 | then composer --working-dir=$DRUPAL_BUILD_DIR require zendframework/zend-stdlib:3.2.1; 96 | fi 97 | 98 | # For Drupal < 8.8 we have to manually upgrade phpunit to avoid PHP 7.3 99 | # incompatibilities. 100 | - if [[ "$DRUPAL_CORE" = "8.6.x" || "$DRUPAL_CORE" = "8.7.x" ]]; 101 | then composer --working-dir=$DRUPAL_BUILD_DIR run-script drupal-phpunit-upgrade; 102 | fi 103 | 104 | script: 105 | # Run the unit tests using phpdbg if the environment variable is 'true'. 106 | - if [[ "$WITH_PHPDBG_COVERAGE" == "true" ]]; 107 | then phpdbg -qrr $DRUPAL_BUILD_DIR/vendor/bin/phpunit --configuration $DRUPAL_BUILD_DIR/core/phpunit.xml --coverage-clover $TRAVIS_BUILD_DIR/coverage.xml $TRAVIS_BUILD_DIR; 108 | fi 109 | 110 | # Run the unit tests with standard php otherwise. 111 | - if [[ "$WITH_PHPDBG_COVERAGE" != "true" ]]; 112 | then $DRUPAL_BUILD_DIR/vendor/bin/phpunit --configuration $DRUPAL_BUILD_DIR/core/phpunit.xml $TRAVIS_BUILD_DIR; 113 | fi 114 | 115 | after_success: 116 | - if [[ "$WITH_PHPDBG_COVERAGE" == "true" ]]; 117 | then bash <(curl -s https://codecov.io/bash); 118 | fi 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Views for Drupal 2 | 3 | [![Build Status](https://img.shields.io/travis/drupal-graphql/graphql-views.svg)](https://travis-ci.org/drupal-graphql/graphql-views) 4 | [![Code Coverage](https://img.shields.io/codecov/c/github/drupal-graphql/graphql-views.svg)](https://codecov.io/gh/drupal-graphql/graphql-views) 5 | [![Code Quality](https://img.shields.io/scrutinizer/g/drupal-graphql/graphql-views.svg)](https://scrutinizer-ci.com/g/drupal-graphql/graphql-views/?branch=8.x-1.x) 6 | 7 | [Drupal GraphQL]: https://github.com/drupal-graphql/graphql 8 | 9 | With `graphql_views` enabled a `GraphQL` views display can be added to any view in the system. 10 | 11 | Results can be sorted, filtered based on content fields, and relationships can be added. There is also the option to return either the full entities, just a selection of fields, or even search results taken straight from a search server. 12 | 13 | Any `GraphQL` views display will provide a field that will adapt to the views configuration: 14 | 15 | - The fields name will be composed of the views and displays machine names or configured manually. 16 | - If the view is configured with pagination, the field will accept pager arguments and return the result list and count field instead of the entity list directly. 17 | - Any exposed filters will be added to the `filters` input type that can be used to pass filter values into the view. 18 | - Any contextual filters will be added to the `contextual_filters` input type. 19 | - If a contextual filters validation criteria match an existing GraphQL type, the field will be added to this type too, and the value will be populated from the current result context. 20 | 21 | Read more on: 22 | - https://www.amazeelabs.com/en/blog/graphql-drupalers-part-4-fetching-entities 23 | - https://www.amazeelabs.com/en/blog/drupal-graphql-batteries-included 24 | 25 | Please also refer to the main [Drupal GraphQL] module for further information. 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drupal/graphql_views", 3 | "type": "drupal-module", 4 | "description": "Exposes your Drupal Views data model through a GraphQL schema.", 5 | "homepage": "http://drupal.org/project/graphql_views", 6 | "license": "GPL-2.0+", 7 | "minimum-stability": "dev" 8 | } 9 | -------------------------------------------------------------------------------- /config/schema/graphql_views.views.schema.yml: -------------------------------------------------------------------------------- 1 | views.display.graphql: 2 | type: views_display 3 | label: 'GraphQL display options' 4 | mapping: 5 | graphql_query_name: 6 | type: string 7 | label: 'GraphQL query name' 8 | -------------------------------------------------------------------------------- /graphql_views.info.yml: -------------------------------------------------------------------------------- 1 | name: GraphQL Views 2 | type: module 3 | description: 'Adds support for views.' 4 | package: GraphQL 5 | core: 8.x 6 | core_version_requirement: ^8 || ^9 7 | dependencies: 8 | - graphql:graphql_core 9 | - drupal:views 10 | - drupal:system (>=8.4) -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Default PHP CodeSniffer configuration for GraphQL. 4 | . 5 | 6 | 7 | 8 | 9 | 10 | src/Annotation 11 | src/Core/Annotation 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Plugin/Deriver/Enums/ViewSortByDeriver.php: -------------------------------------------------------------------------------- 1 | entityTypeManager->hasDefinition('view')) { 16 | $viewStorage = $this->entityTypeManager->getStorage('view'); 17 | 18 | foreach (Views::getApplicableViews('graphql_display') as list($viewId, $displayId)) { 19 | /** @var \Drupal\views\ViewEntityInterface $view */ 20 | $view = $viewStorage->load($viewId); 21 | if (!$type = $this->getRowResolveType($view, $displayId)) { 22 | continue; 23 | } 24 | 25 | /** @var \Drupal\graphql_views\Plugin\views\display\GraphQL $display */ 26 | $display = $this->getViewDisplay($view, $displayId); 27 | $sorts = array_filter($display->getOption('sorts') ?: [], function ($sort) { 28 | return $sort['exposed']; 29 | }); 30 | $sorts = array_reduce($sorts, function ($carry, $sort) { 31 | $carry[strtoupper($sort['id'])] = [ 32 | 'value' => $sort['id'], 33 | 'description' => $sort['expose']['label'], 34 | ]; 35 | return $carry; 36 | }, []); 37 | 38 | if (!empty($sorts)) { 39 | $id = implode('-', [$viewId, $displayId, 'view']); 40 | $this->derivatives["$viewId-$displayId"] = [ 41 | 'name' => StringHelper::camelCase($id, 'sort', 'by'), 42 | 'values' => $sorts, 43 | ] + $basePluginDefinition; 44 | } 45 | } 46 | } 47 | 48 | return parent::getDerivativeDefinitions($basePluginDefinition); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/Plugin/Deriver/Fields/ViewDeriver.php: -------------------------------------------------------------------------------- 1 | entityTypeManager->hasDefinition('view')) { 19 | $viewStorage = $this->entityTypeManager->getStorage('view'); 20 | 21 | foreach (Views::getApplicableViews('graphql_display') as list($viewId, $displayId)) { 22 | /** @var \Drupal\views\ViewEntityInterface $view */ 23 | $view = $viewStorage->load($viewId); 24 | if (!$this->getRowResolveType($view, $displayId)) { 25 | continue; 26 | } 27 | 28 | /** @var \Drupal\graphql_views\Plugin\views\display\GraphQL $display */ 29 | $display = $this->getViewDisplay($view, $displayId); 30 | 31 | $id = implode('-', [$viewId, $displayId, 'view']); 32 | $info = $this->getArgumentsInfo($display->getOption('arguments') ?: []); 33 | $arguments = []; 34 | $arguments += $this->getContextualArguments($info, $id); 35 | $arguments += $this->getPagerArguments($display); 36 | $arguments += $this->getSortArguments($display, $id); 37 | $arguments += $this->getFilterArguments($display, $id); 38 | $types = $this->getTypes($info); 39 | 40 | $this->derivatives[$id] = [ 41 | 'id' => $id, 42 | 'name' => $display->getGraphQLQueryName(), 43 | 'type' => $display->getGraphQLResultName(), 44 | 'parents' => $types, 45 | 'arguments' => $arguments, 46 | 'view' => $viewId, 47 | 'display' => $displayId, 48 | 'paged' => $this->isPaged($display), 49 | 'arguments_info' => $info, 50 | ] + $this->getCacheMetadataDefinition($view, $display) + $basePluginDefinition; 51 | } 52 | } 53 | 54 | return parent::getDerivativeDefinitions($basePluginDefinition); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/Plugin/Deriver/Fields/ViewResultCountDeriver.php: -------------------------------------------------------------------------------- 1 | entityTypeManager->hasDefinition('view')) { 18 | $viewStorage = $this->entityTypeManager->getStorage('view'); 19 | 20 | foreach (Views::getApplicableViews('graphql_display') as list($viewId, $displayId)) { 21 | /** @var \Drupal\views\ViewEntityInterface $view */ 22 | $view = $viewStorage->load($viewId); 23 | /** @var \Drupal\graphql_views\Plugin\views\display\GraphQL $display */ 24 | $display = $this->getViewDisplay($view, $displayId); 25 | if (!$this->isPaged($display)) { 26 | continue; 27 | } 28 | 29 | if (!$this->getRowResolveType($view, $displayId)) { 30 | continue; 31 | } 32 | 33 | $id = implode('-', [$viewId, $displayId, 'result', 'count']); 34 | $this->derivatives[$id] = [ 35 | 'id' => $id, 36 | 'type' => 'Int', 37 | 'parents' => [$display->getGraphQLResultName()], 38 | 'view' => $viewId, 39 | 'display' => $displayId, 40 | ] + $this->getCacheMetadataDefinition($view, $display) + $basePluginDefinition; 41 | } 42 | } 43 | 44 | return parent::getDerivativeDefinitions($basePluginDefinition); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/Plugin/Deriver/Fields/ViewResultListDeriver.php: -------------------------------------------------------------------------------- 1 | entityTypeManager->hasDefinition('view')) { 19 | $viewStorage = $this->entityTypeManager->getStorage('view'); 20 | 21 | foreach (Views::getApplicableViews('graphql_display') as list($viewId, $displayId)) { 22 | /** @var \Drupal\views\ViewEntityInterface $view */ 23 | $view = $viewStorage->load($viewId); 24 | if (!$type = $this->getRowResolveType($view, $displayId)) { 25 | continue; 26 | } 27 | 28 | /** @var \Drupal\graphql_views\Plugin\views\display\GraphQL $display */ 29 | $display = $this->getViewDisplay($view, $displayId); 30 | 31 | $id = implode('-', [$viewId, $displayId, 'result', 'list']); 32 | $style = $this->getViewStyle($view, $displayId); 33 | $this->derivatives[$id] = [ 34 | 'id' => $id, 35 | 'type' => StringHelper::listType($type), 36 | 'parents' => [$display->getGraphQLResultName()], 37 | 'view' => $viewId, 38 | 'display' => $displayId, 39 | 'uses_fields' => $style->usesFields(), 40 | ] + $this->getCacheMetadataDefinition($view, $display) + $basePluginDefinition; 41 | } 42 | } 43 | 44 | return parent::getDerivativeDefinitions($basePluginDefinition); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/Plugin/Deriver/Fields/ViewRowFieldDeriver.php: -------------------------------------------------------------------------------- 1 | entityTypeManager->hasDefinition('view')) { 19 | $viewStorage = $this->entityTypeManager->getStorage('view'); 20 | 21 | foreach (Views::getApplicableViews('graphql_display') as list($viewId, $displayId)) { 22 | /** @var \Drupal\views\ViewEntityInterface $view */ 23 | $view = $viewStorage->load($viewId); 24 | /** @var \Drupal\graphql_views\Plugin\views\display\GraphQL $display */ 25 | $display = $this->getViewDisplay($view, $displayId); 26 | $rowPlugin = $display->getPlugin('row'); 27 | 28 | // This deriver only supports our custom field row plugin. 29 | if (!$rowPlugin instanceof GraphQLFieldRow) { 30 | continue; 31 | } 32 | 33 | foreach ($display->getHandlers('field') as $name => $field) { 34 | $id = implode('-', [$viewId, $displayId, 'field', $name]); 35 | $alias = $rowPlugin->getFieldKeyAlias($name); 36 | $type = $rowPlugin->getFieldType($name); 37 | 38 | $this->derivatives[$id] = [ 39 | 'id' => $id, 40 | 'name' => $alias, 41 | 'type' => $type, 42 | 'parents' => [$display->getGraphQLRowName()], 43 | 'view' => $viewId, 44 | 'display' => $displayId, 45 | 'field' => $alias, 46 | ] + $this->getCacheMetadataDefinition($view, $display) + $basePluginDefinition; 47 | } 48 | } 49 | } 50 | 51 | return parent::getDerivativeDefinitions($basePluginDefinition); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/Plugin/Deriver/InputTypes/ViewContextualFilterInputDeriver.php: -------------------------------------------------------------------------------- 1 | entityTypeManager->hasDefinition('view')) { 20 | $viewStorage = $this->entityTypeManager->getStorage('view'); 21 | 22 | foreach (Views::getApplicableViews('graphql_display') as list($viewId, $displayId)) { 23 | /** @var \Drupal\views\ViewEntityInterface $view */ 24 | $view = $viewStorage->load($viewId); 25 | if (!$this->getRowResolveType($view, $displayId)) { 26 | continue; 27 | } 28 | 29 | $display = $this->getViewDisplay($view, $displayId); 30 | $argumentsInfo = $this->getArgumentsInfo($display->getOption('arguments') ?: []); 31 | if (!empty($argumentsInfo)) { 32 | $id = implode('_', [ 33 | $viewId, $displayId, 'view', 'contextual', 'filter', 'input', 34 | ]); 35 | 36 | $this->derivatives[$id] = [ 37 | 'id' => $id, 38 | 'name' => StringHelper::camelCase($viewId, $displayId, 'view', 'contextual', 'filter', 'input'), 39 | 'fields' => array_fill_keys(array_keys($argumentsInfo), [ 40 | 'type' => 'String', 41 | ]), 42 | 'view' => $viewId, 43 | 'display' => $displayId, 44 | ] + $this->getCacheMetadataDefinition($view, $display) + $basePluginDefinition; 45 | } 46 | } 47 | } 48 | 49 | return parent::getDerivativeDefinitions($basePluginDefinition); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/Plugin/Deriver/InputTypes/ViewFilterInputDeriver.php: -------------------------------------------------------------------------------- 1 | entityTypeManager->hasDefinition('view')) { 20 | $viewStorage = $this->entityTypeManager->getStorage('view'); 21 | 22 | foreach (Views::getApplicableViews('graphql_display') as list($viewId, $displayId)) { 23 | /** @var \Drupal\views\ViewEntityInterface $view */ 24 | $view = $viewStorage->load($viewId); 25 | if (!$this->getRowResolveType($view, $displayId)) { 26 | continue; 27 | } 28 | 29 | /** @var \Drupal\graphql_views\Plugin\views\display\GraphQL $display */ 30 | $display = $this->getViewDisplay($view, $displayId); 31 | $id = implode('_', [$viewId, $displayId, 'view', 'filter', 'input']); 32 | 33 | // Re-key filters by filter identifier. 34 | $filters = array_reduce(array_filter($display->getOption('filters') ?: [], function($filter) { 35 | return array_key_exists('exposed', $filter) && $filter['exposed']; 36 | }), function($carry, $current) { 37 | return $carry + [ 38 | $current['expose']['identifier'] => $current, 39 | ]; 40 | }, []); 41 | 42 | // If there are no exposed filters, don't create the derivative. 43 | if (empty($filters)) { 44 | continue; 45 | } 46 | 47 | $fields = array_map(function($filter) use ($basePluginDefinition) { 48 | if ($this->isGenericInputFilter($filter)) { 49 | return $this->createGenericInputFilterDefinition($filter, $basePluginDefinition); 50 | } 51 | 52 | return [ 53 | 'type' => $filter['expose']['multiple'] ? StringHelper::listType('String') : 'String', 54 | ]; 55 | }, $filters); 56 | 57 | $this->derivatives[$id] = [ 58 | 'id' => $id, 59 | 'name' => $display->getGraphQLFilterInputName(), 60 | 'fields' => $fields, 61 | 'view' => $viewId, 62 | 'display' => $displayId, 63 | ] + $this->getCacheMetadataDefinition($view, $display) + $basePluginDefinition; 64 | } 65 | 66 | } 67 | return parent::getDerivativeDefinitions($basePluginDefinition); 68 | } 69 | 70 | /** 71 | * Checks if a filter definition is a generic input filter. 72 | * 73 | * @param mixed $filter 74 | * $filter['value'] = []; 75 | * $filter['value'] = [ 76 | * "text", 77 | * "test" 78 | * ]; 79 | * $filter['value'] = [ 80 | * 'distance' => 10, 81 | * 'distance2' => 30, 82 | * ... 83 | * ]; 84 | * @return bool 85 | */ 86 | public function isGenericInputFilter($filter) { 87 | if (!is_array($filter['value']) || count($filter['value']) == 0) { 88 | return false; 89 | } 90 | 91 | $firstKey = array_keys($filter['value'])[0]; 92 | return is_string($firstKey); 93 | } 94 | 95 | /** 96 | * Creates a definition for a generic input filter. 97 | * 98 | * @param mixed $filter 99 | * $filter['value'] = []; 100 | * $filter['value'] = [ 101 | * "text", 102 | * "test" 103 | * ]; 104 | * $filter['value'] = [ 105 | * 'distance' => 10, 106 | * 'distance2' => 30, 107 | * ... 108 | * ]; 109 | * @param mixed $basePluginDefinition 110 | * @return array 111 | */ 112 | public function createGenericInputFilterDefinition($filter, $basePluginDefinition) { 113 | $filterId = $filter['expose']['identifier']; 114 | 115 | $id = implode('_', [ 116 | $filter['expose']['multiple'] ? $filterId : $filterId . '_multi', 117 | 'view', 118 | 'filter', 119 | 'input', 120 | ]); 121 | 122 | $fields = []; 123 | foreach ($filter['value'] as $fieldKey => $fieldDefaultValue) { 124 | $fields[$fieldKey] = [ 125 | 'type' => 'String', 126 | ]; 127 | } 128 | 129 | $genericInputFilter = [ 130 | 'id' => $id, 131 | 'name' => StringHelper::camelCase($id), 132 | 'fields' => $fields, 133 | ] + $basePluginDefinition; 134 | 135 | $this->derivatives[$id] = $genericInputFilter; 136 | 137 | return [ 138 | 'type' => $filter['expose']['multiple'] ? StringHelper::listType($genericInputFilter['name']) : $genericInputFilter['name'], 139 | ]; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Plugin/Deriver/Types/ViewResultTypeDeriver.php: -------------------------------------------------------------------------------- 1 | entityTypeManager->hasDefinition('view')) { 18 | $viewStorage = $this->entityTypeManager->getStorage('view'); 19 | 20 | foreach (Views::getApplicableViews('graphql_display') as list($viewId, $displayId)) { 21 | /** @var \Drupal\views\ViewEntityInterface $view */ 22 | $view = $viewStorage->load($viewId); 23 | if (!$this->getRowResolveType($view, $displayId)) { 24 | continue; 25 | } 26 | 27 | /** @var \Drupal\graphql_views\Plugin\views\display\GraphQL $display */ 28 | $display = $this->getViewDisplay($view, $displayId); 29 | 30 | $id = implode('-', [$viewId, $displayId, 'result']); 31 | $this->derivatives[$id] = [ 32 | 'id' => $id, 33 | 'name' => $display->getGraphQLResultName(), 34 | 'view' => $viewId, 35 | 'display' => $displayId, 36 | ] + $this->getCacheMetadataDefinition($view, $display) + $basePluginDefinition; 37 | } 38 | } 39 | 40 | return parent::getDerivativeDefinitions($basePluginDefinition); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/Plugin/Deriver/Types/ViewRowTypeDeriver.php: -------------------------------------------------------------------------------- 1 | entityTypeManager->hasDefinition('view')) { 18 | $viewStorage = $this->entityTypeManager->getStorage('view'); 19 | 20 | foreach (Views::getApplicableViews('graphql_display') as list($viewId, $displayId)) { 21 | /** @var \Drupal\views\ViewEntityInterface $view */ 22 | $view = $viewStorage->load($viewId); 23 | if (!$this->getRowResolveType($view, $displayId)) { 24 | continue; 25 | } 26 | 27 | $style = $this->getViewStyle($view, $displayId); 28 | // This deriver only supports style plugins that use fields. 29 | if (!$style->usesFields()) { 30 | continue; 31 | } 32 | 33 | /** @var \Drupal\graphql_views\Plugin\views\display\GraphQL $display */ 34 | $display = $this->getViewDisplay($view, $displayId); 35 | 36 | $id = implode('-', [$viewId, $displayId, 'row']); 37 | $this->derivatives[$id] = [ 38 | 'id' => $id, 39 | 'name' => $display->getGraphQLRowName(), 40 | 'view' => $viewId, 41 | 'display' => $displayId, 42 | ] + $this->getCacheMetadataDefinition($view, $display) + $basePluginDefinition; 43 | } 44 | } 45 | 46 | return parent::getDerivativeDefinitions($basePluginDefinition); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/Plugin/Deriver/ViewDeriverBase.php: -------------------------------------------------------------------------------- 1 | get('entity_type.manager'), 52 | $container->get('plugin.manager.graphql.interface') 53 | ); 54 | } 55 | 56 | /** 57 | * Creates a ViewDeriver object. 58 | * 59 | * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager 60 | * An entity type manager instance. 61 | * @param \Drupal\Component\Plugin\PluginManagerInterface $interfacePluginManager 62 | * The plugin manager for graphql interfaces. 63 | */ 64 | public function __construct( 65 | EntityTypeManagerInterface $entityTypeManager, 66 | PluginManagerInterface $interfacePluginManager 67 | ) { 68 | $this->interfacePluginManager = $interfacePluginManager; 69 | $this->entityTypeManager = $entityTypeManager; 70 | } 71 | 72 | /** 73 | * Retrieves the entity type id of an entity by its base or data table. 74 | * 75 | * @param string $table 76 | * The base or data table of an entity. 77 | * 78 | * @return string 79 | * The id of the entity type that the given base table belongs to. 80 | */ 81 | protected function getEntityTypeByTable($table) { 82 | if (!isset($this->dataTables)) { 83 | $this->dataTables = []; 84 | 85 | foreach ($this->entityTypeManager->getDefinitions() as $entityTypeId => $entityType) { 86 | if ($dataTable = $entityType->getDataTable()) { 87 | $this->dataTables[$dataTable] = $entityType->id(); 88 | } 89 | if ($baseTable = $entityType->getBaseTable()) { 90 | $this->dataTables[$baseTable] = $entityType->id(); 91 | } 92 | } 93 | } 94 | 95 | return !empty($this->dataTables[$table]) ? $this->dataTables[$table] : NULL; 96 | } 97 | 98 | /** 99 | * Retrieves the type the view's rows resolve to. 100 | * 101 | * @param \Drupal\views\ViewEntityInterface $view 102 | * The view entity. 103 | * @param string $displayId 104 | * Interface plugin manager. 105 | * 106 | * @return null|string 107 | * The name of the type or NULL if the type could not be derived. 108 | */ 109 | protected function getRowResolveType(ViewEntityInterface $view, $displayId) { 110 | return $this->traitGetRowResolveType($view, $displayId, $this->interfacePluginManager); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/Plugin/GraphQL/Enums/ViewSortBy.php: -------------------------------------------------------------------------------- 1 | [ 22 | 'value' => 'ASC', 23 | 'description' => 'Sort in ascending order.', 24 | ], 25 | 'DESC' => [ 26 | 'value' => 'DESC', 27 | 'description' => 'Sort in descending order.', 28 | ], 29 | ]; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/Plugin/GraphQL/Fields/Entity/Fields/View/ViewDerivative.php: -------------------------------------------------------------------------------- 1 | getValue(); 64 | $this->pluginDefinition['view'] = $values['target_id']; 65 | $this->pluginDefinition['display'] = $values['display_id']; 66 | $view = EntityView::load($values['target_id']); 67 | $display = $this->getViewDisplay($view, $values['display_id']); 68 | $this->pluginDefinition['paged'] = $this->isPaged($display); 69 | $this->pluginDefinition['arguments_info'] = $this->getArgumentsInfo($display->getOption('arguments') ?: []); 70 | $this->pluginDefinition = array_merge($this->pluginDefinition, $this->getCacheMetadataDefinition($view, $display)); 71 | $this->setOverridenViewDefaults($value, $args); 72 | $this->setViewDefaultValues($display, $args); 73 | return parent::resolveValues($value, $args, $context, $info); 74 | } 75 | 76 | /** 77 | * Get configuration values from views reference field. 78 | * 79 | * @param mixed $value 80 | * The current object value. 81 | * 82 | * @return array|mixed 83 | * Return unserialized data. 84 | */ 85 | protected function getViewReferenceConfiguration($value) { 86 | $values = $value->getValue(); 87 | return isset($values['data']) ? unserialize($values['data']) : []; 88 | } 89 | 90 | /** 91 | * Set default display settings. 92 | * 93 | * @param mixed $value 94 | * The current object value. 95 | * @param array $args 96 | * Arguments where the default view settings needs to be added. 97 | */ 98 | protected function setOverridenViewDefaults($value, array &$args) { 99 | $viewReferenceConfiguration = $this->getViewReferenceConfiguration($value); 100 | if (!empty($viewReferenceConfiguration['pager'])) { 101 | $this->pluginDefinition['paged'] = in_array($viewReferenceConfiguration['pager'], [ 102 | 'full', 103 | 'mini', 104 | ]); 105 | } 106 | 107 | if (!isset($args['pageSize']) && !empty($viewReferenceConfiguration['limit'])) { 108 | $args['pageSize'] = $viewReferenceConfiguration['limit']; 109 | } 110 | 111 | if (!isset($args['offset']) && !empty($viewReferenceConfiguration['offset'])) { 112 | $args['offset'] = $viewReferenceConfiguration['offset']; 113 | } 114 | 115 | /* Expected format: {"contextualFilter": {"key": "value","keyN": "valueN"}} */ 116 | if (!isset($args['contextualFilter']) && !empty($viewReferenceConfiguration['argument'])) { 117 | $argument = json_decode($viewReferenceConfiguration['argument'], TRUE); 118 | if (isset($argument['contextualFilter']) && !empty($argument['contextualFilter'])) { 119 | $args['contextualFilter'] = $argument['contextualFilter']; 120 | } 121 | } 122 | } 123 | 124 | /** 125 | * Set default display settings. 126 | * 127 | * @param \Drupal\views\Plugin\views\display\DisplayPluginInterface $display 128 | * The display configuration. 129 | * @param array $args 130 | * Arguments where the default view settings needs to be added. 131 | */ 132 | protected function setViewDefaultValues(DisplayPluginInterface $display, array &$args) { 133 | if (!isset($args['pageSize']) && $this->pluginDefinition['paged']) { 134 | $args['pageSize'] = $this->getPagerLimit($display); 135 | } 136 | if (!isset($args['page']) && $this->pluginDefinition['paged']) { 137 | $args['page'] = $this->getPagerOffset($display); 138 | } 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /src/Plugin/GraphQL/Fields/View.php: -------------------------------------------------------------------------------- 1 | entityTypeManager = $entityTypeManager; 45 | parent::__construct($configuration, $pluginId, $pluginDefinition); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public static function create(ContainerInterface $container, array $configuration, $pluginId, $pluginDefinition) { 52 | return new static( 53 | $configuration, 54 | $pluginId, 55 | $pluginDefinition, 56 | $container->get('entity_type.manager') 57 | ); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function resolveValues($value, array $args, ResolveContext $context, ResolveInfo $info) { 64 | $storage = $this->entityTypeManager->getStorage('view'); 65 | $definition = $this->getPluginDefinition(); 66 | 67 | /** @var \Drupal\views\Entity\View $view */ 68 | if ($view = $storage->load($definition['view'])) { 69 | $executable = $view->getExecutable(); 70 | $executable->setDisplay($definition['display']); 71 | 72 | // Set view contextual filters. 73 | /* @see \Drupal\graphql_views\ViewDeriverHelperTrait::getArgumentsInfo() */ 74 | if (!empty($definition['arguments_info'])) { 75 | $arguments = $this->extractContextualFilters($value, $args); 76 | $executable->setArguments($arguments); 77 | } 78 | 79 | $filters = $executable->getDisplay()->getOption('filters');; 80 | $input = $this->extractExposedInput($value, $args, $filters); 81 | $executable->setExposedInput($input); 82 | 83 | // This is a workaround for the Taxonomy Term filter which requires a full 84 | // exposed form to be sent OR the display being an attachment to just 85 | // accept input values. 86 | $executable->is_attachment = TRUE; 87 | $executable->exposed_raw_input = $input; 88 | 89 | if (!empty($definition['paged'])) { 90 | // Set paging parameters. 91 | $executable->setItemsPerPage($args['pageSize']); 92 | $executable->setCurrentPage($args['page']); 93 | } 94 | 95 | if (isset($args['offset']) && !empty($args['offset'])) { 96 | $executable->setOffset($args['offset']); 97 | } 98 | 99 | $result = $executable->render($definition['display']); 100 | /** @var \Drupal\Core\Cache\CacheableMetadata $cache */ 101 | if ($cache = $result['cache']) { 102 | $cache->setCacheContexts( 103 | array_filter($cache->getCacheContexts(), function ($context) { 104 | // Don't emit the url cache contexts. 105 | return $context !== 'url' && strpos($context, 'url.') !== 0; 106 | }) 107 | ); 108 | } 109 | yield $result; 110 | } 111 | } 112 | 113 | /** 114 | * {@inheritdoc} 115 | */ 116 | protected function getCacheDependencies(array $result, $value, array $args, ResolveContext $context, ResolveInfo $info) { 117 | return array_map(function ($item) { 118 | return $item['cache']; 119 | }, $result); 120 | } 121 | 122 | /** 123 | * Retrieves the contextual filter argument from the parent value or args. 124 | * 125 | * @param $value 126 | * The resolved parent value. 127 | * @param $args 128 | * The arguments provided to the field. 129 | * 130 | * @return array 131 | * An array of arguments containing the contextual filter value from the 132 | * parent or provided args if any. 133 | */ 134 | protected function extractContextualFilters($value, $args) { 135 | $definition = $this->getPluginDefinition(); 136 | $arguments = []; 137 | 138 | foreach ($definition['arguments_info'] as $argumentId => $argumentInfo) { 139 | if (isset($args['contextualFilter'][$argumentId])) { 140 | $arguments[$argumentInfo['index']] = $args['contextualFilter'][$argumentId]; 141 | } 142 | elseif ( 143 | $value instanceof EntityInterface && 144 | $value->getEntityTypeId() === $argumentInfo['entity_type'] && 145 | (empty($argumentInfo['bundles']) || 146 | in_array($value->bundle(), $argumentInfo['bundles'], TRUE)) 147 | ) { 148 | $arguments[$argumentInfo['index']] = $value->id(); 149 | } 150 | else { 151 | $arguments[$argumentInfo['index']] = NULL; 152 | } 153 | } 154 | 155 | return $arguments; 156 | } 157 | 158 | /** 159 | * Retrieves sort and filter arguments from the provided field args. 160 | * 161 | * @param $value 162 | * The resolved parent value. 163 | * @param $args 164 | * The array of arguments provided to the field. 165 | * @param $filters 166 | * The available filters for the configured view. 167 | * 168 | * @return array 169 | * The array of sort and filter arguments to execute the view with. 170 | */ 171 | protected function extractExposedInput($value, $args, $filters) { 172 | // Prepare arguments for use as exposed form input. 173 | $input = array_filter([ 174 | // Sorting arguments. 175 | 'sort_by' => isset($args['sortBy']) ? $args['sortBy'] : NULL, 176 | 'sort_order' => isset($args['sortDirection']) ? $args['sortDirection'] : NULL, 177 | ]); 178 | 179 | // If some filters are missing from the input, set them to an empty string 180 | // explicitly. Otherwise views module generates "Undefined index" notice. 181 | foreach ($filters as $filterKey => $filterRow) { 182 | if (!isset($filterRow['expose']['identifier'])) { 183 | continue; 184 | } 185 | 186 | $inputKey = $filterRow['expose']['identifier']; 187 | if (!isset($args['filter'][$inputKey])) { 188 | $input[$inputKey] = $filterRow['value']; 189 | } else { 190 | $input[$inputKey] = $args['filter'][$inputKey]; 191 | } 192 | } 193 | 194 | return $input; 195 | } 196 | 197 | } 198 | -------------------------------------------------------------------------------- /src/Plugin/GraphQL/Fields/ViewResultCount.php: -------------------------------------------------------------------------------- 1 | total_rows); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Plugin/GraphQL/Fields/ViewResultList.php: -------------------------------------------------------------------------------- 1 | getPluginDefinition(); 26 | if (isset($value[$definition['field']])) { 27 | yield $value[$definition['field']]; 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/Plugin/GraphQL/InputTypes/ViewContextualFilterInput.php: -------------------------------------------------------------------------------- 1 | pluginDefinition['view'] === $view->id() && $this->pluginDefinition['display'] == $view->current_display) { 29 | return TRUE; 30 | } 31 | } 32 | 33 | return parent::applies($object, $context, $info); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Plugin/GraphQL/Types/ViewRowType.php: -------------------------------------------------------------------------------- 1 | '']; 101 | return $options; 102 | } 103 | 104 | /** 105 | * Get the user defined query name or the default one. 106 | * 107 | * @return string 108 | * Query name. 109 | */ 110 | public function getGraphQLQueryName() { 111 | return $this->getGraphQLName(); 112 | } 113 | 114 | /** 115 | * Gets the result name. 116 | * 117 | * @return string 118 | * Result name. 119 | */ 120 | public function getGraphQLResultName() { 121 | return $this->getGraphQLName('result', TRUE); 122 | } 123 | 124 | /** 125 | * Gets the row name. 126 | * 127 | * @return string 128 | * Row name. 129 | */ 130 | public function getGraphQLRowName() { 131 | return $this->getGraphQLName('row', TRUE); 132 | } 133 | 134 | /** 135 | * Gets the filter input name.. 136 | * 137 | * @return string 138 | * Result name. 139 | */ 140 | public function getGraphQLFilterInputName() { 141 | return $this->getGraphQLName('filter_input', TRUE); 142 | } 143 | 144 | /** 145 | * Gets the contextual filter input name. 146 | * 147 | * @return string 148 | * Result name. 149 | */ 150 | public function getGraphQLContextualFilterInputName() { 151 | return $this->getGraphQLName('contextual_filter_input', TRUE); 152 | } 153 | 154 | /** 155 | * Returns the formatted name. 156 | * 157 | * @param string|null $suffix 158 | * Id suffix, eg. row, result. 159 | * @param bool $type 160 | * Whether to use camel- or snake case. Uses camel case if TRUE. Defaults to 161 | * FALSE. 162 | * 163 | * @return string The id. 164 | * The id. 165 | */ 166 | public function getGraphQLName($suffix = NULL, $type = FALSE) { 167 | $queryName = strip_tags($this->getOption('graphql_query_name')); 168 | 169 | if (empty($queryName)) { 170 | $viewId = $this->view->id(); 171 | $displayId = $this->display['id']; 172 | $parts = [$viewId, $displayId, 'view', $suffix]; 173 | return $type ? call_user_func_array([StringHelper::class, 'camelCase'], $parts) : call_user_func_array([StringHelper::class, 'propCase'], $parts); 174 | } 175 | 176 | $parts = array_filter([$queryName, $suffix]); 177 | return $type ? call_user_func_array([StringHelper::class, 'camelCase'], $parts) : call_user_func_array([StringHelper::class, 'propCase'], $parts); 178 | } 179 | 180 | /** 181 | * {@inheritdoc} 182 | */ 183 | public function optionsSummary(&$categories, &$options) { 184 | parent::optionsSummary($categories, $options); 185 | 186 | unset($categories['title']); 187 | unset($categories['pager'], $categories['exposed'], $categories['access']); 188 | 189 | unset($options['show_admin_links'], $options['analyze-theme'], $options['link_display']); 190 | unset($options['show_admin_links'], $options['analyze-theme'], $options['link_display']); 191 | 192 | unset($options['title'], $options['access']); 193 | unset($options['exposed_block'], $options['css_class']); 194 | unset($options['query'], $options['group_by']); 195 | 196 | $categories['graphql'] = [ 197 | 'title' => $this->t('GraphQL'), 198 | 'column' => 'second', 199 | 'build' => [ 200 | '#weight' => -10, 201 | ], 202 | ]; 203 | 204 | $options['graphql_query_name'] = [ 205 | 'category' => 'graphql', 206 | 'title' => $this->t('Query name'), 207 | 'value' => views_ui_truncate($this->getGraphQLQueryName(), 24), 208 | ]; 209 | } 210 | 211 | /** 212 | * {@inheritdoc} 213 | */ 214 | public function buildOptionsForm(&$form, FormStateInterface $form_state) { 215 | parent::buildOptionsForm($form, $form_state); 216 | 217 | switch ($form_state->get('section')) { 218 | case 'graphql_query_name': 219 | $form['#title'] .= $this->t('Query name'); 220 | $form['graphql_query_name'] = [ 221 | '#type' => 'textfield', 222 | '#description' => $this->t('This will be the graphQL query name.'), 223 | '#default_value' => $this->getGraphQLQueryName(), 224 | ]; 225 | break; 226 | } 227 | } 228 | 229 | /** 230 | * {@inheritdoc} 231 | */ 232 | public function submitOptionsForm(&$form, FormStateInterface $form_state) { 233 | parent::submitOptionsForm($form, $form_state); 234 | $section = $form_state->get('section'); 235 | switch ($section) { 236 | case 'graphql_query_name': 237 | $this->setOption($section, $form_state->getValue($section)); 238 | break; 239 | } 240 | } 241 | 242 | /** 243 | * {@inheritdoc} 244 | */ 245 | public function execute() { 246 | return $this->view->execute(); 247 | } 248 | 249 | /** 250 | * {@inheritdoc} 251 | */ 252 | public function render() { 253 | $rows = (!empty($this->view->result) || $this->view->style_plugin->evenEmpty()) ? $this->view->style_plugin->render($this->view->result) : []; 254 | 255 | // Apply the cache metadata from the display plugin. This comes back as a 256 | // cache render array so we have to transform it back afterwards. 257 | $this->applyDisplayCacheabilityMetadata($this->view->element); 258 | 259 | return [ 260 | 'view' => $this->view, 261 | 'rows' => $rows, 262 | 'cache' => CacheableMetadata::createFromRenderArray($this->view->element), 263 | ]; 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/Plugin/views/exposed_form/GraphQL.php: -------------------------------------------------------------------------------- 1 | view->exposed_data = $this->view->getExposedInput(); 26 | 27 | return NULL; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Plugin/views/row/GraphQLEntityRow.php: -------------------------------------------------------------------------------- 1 | entityTypeBundleInfo = $entityTypeBundleInfo; 84 | $this->languageManager = $languageManager; 85 | $this->entityTypeManager = $entityTypeManager; 86 | $this->entityRepository = $entityRepository; 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | public static function create(ContainerInterface $container, array $configuration, $pluginId, $pluginDefinition) { 93 | return new static( 94 | $configuration, 95 | $pluginId, 96 | $pluginDefinition, 97 | $container->get('entity_type.bundle.info'), 98 | $container->get('language_manager'), 99 | $container->get('entity_type.manager'), 100 | $container->get('entity.repository') 101 | ); 102 | } 103 | 104 | /** 105 | * {@inheritdoc} 106 | */ 107 | public function render($row) { 108 | if ($entity = $this->getEntityFromRow($row)) { 109 | return $this->view->getBaseEntityType() ? $this->getEntityTranslation($entity, $row) : $entity; 110 | } 111 | 112 | return NULL; 113 | } 114 | 115 | /** 116 | * {@inheritdoc} 117 | */ 118 | protected function getEntityTranslationRenderer() { 119 | if ($this->view->getBaseEntityType()) { 120 | return $this->getEntityTranslationRendererBase(); 121 | } 122 | 123 | return NULL; 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | */ 129 | public function getEntityTypeManager() { 130 | return $this->entityTypeManager; 131 | } 132 | 133 | /** 134 | * {@inheritdoc} 135 | */ 136 | public function getEntityRepository() { 137 | return $this->entityRepository; 138 | } 139 | 140 | /** 141 | * {@inheritdoc} 142 | */ 143 | public function getEntityTypeId() { 144 | if ($entityType = $this->view->getBaseEntityType()) { 145 | return $entityType->id(); 146 | } 147 | 148 | return NULL; 149 | } 150 | 151 | /** 152 | * {@inheritdoc} 153 | */ 154 | protected function getEntityTypeBundleInfo() { 155 | return $this->entityTypeBundleInfo; 156 | } 157 | 158 | /** 159 | * {@inheritdoc} 160 | */ 161 | protected function getLanguageManager() { 162 | return $this->languageManager; 163 | } 164 | 165 | /** 166 | * Retrieves the entity object from a result row. 167 | * 168 | * @param \Drupal\Views\ResultRow $row 169 | * The views result row object. 170 | * 171 | * @return null|\Drupal\Core\Entity\EntityInterface 172 | * The extracted entity object or NULL if it could not be retrieved. 173 | */ 174 | protected function getEntityFromRow(ResultRow $row) { 175 | if (isset($row->_entity) && $row->_entity instanceof EntityInterface) { 176 | return $row->_entity; 177 | } 178 | 179 | if (isset($row->_object) && $row->_object instanceof EntityAdapter) { 180 | return $row->_object->getValue(); 181 | } 182 | 183 | return NULL; 184 | } 185 | 186 | /** 187 | * {@inheritdoc} 188 | */ 189 | protected function getView() { 190 | return $this->view; 191 | } 192 | 193 | /** 194 | * {@inheritdoc} 195 | */ 196 | public function query() { 197 | parent::query(); 198 | 199 | if ($this->view->getBaseEntityType()) { 200 | $this->getEntityTranslationRenderer()->query($this->view->getQuery()); 201 | } 202 | } 203 | 204 | } 205 | -------------------------------------------------------------------------------- /src/Plugin/views/row/GraphQLFieldRow.php: -------------------------------------------------------------------------------- 1 | options['field_options'])) { 55 | $options = (array) $this->options['field_options']; 56 | // Prepare a trimmed version of replacement aliases. 57 | $aliases = static::extractFromOptionsArray('alias', $options); 58 | $this->replacementAliases = array_filter(array_map('trim', $aliases)); 59 | // Prepare an array of raw output field options. 60 | $this->rawOutputOptions = static::extractFromOptionsArray('raw_output', $options); 61 | $this->typeOptions = static::extractFromOptionsArray('type', $options); 62 | } 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | protected function defineOptions() { 69 | $options = parent::defineOptions(); 70 | $options['field_options'] = ['default' => []]; 71 | 72 | return $options; 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function buildOptionsForm(&$form, FormStateInterface $form_state) { 79 | parent::buildOptionsForm($form, $form_state); 80 | 81 | $form['field_options'] = [ 82 | '#type' => 'table', 83 | '#header' => [ 84 | $this->t('Field'), 85 | $this->t('Alias'), 86 | $this->t('Raw output'), 87 | $this->t('Type'), 88 | ], 89 | '#empty' => $this->t('You have no fields. Add some to your view.'), 90 | '#tree' => TRUE, 91 | ]; 92 | 93 | $options = $this->options['field_options']; 94 | 95 | if ($fields = $this->view->display_handler->getOption('fields')) { 96 | foreach ($fields as $id => $field) { 97 | // Don't show the field if it has been excluded. 98 | if (!empty($field['exclude'])) { 99 | continue; 100 | } 101 | 102 | $form['field_options'][$id]['field'] = [ 103 | '#markup' => $id, 104 | ]; 105 | 106 | $form['field_options'][$id]['alias'] = [ 107 | '#title' => $this->t('Alias for @id', ['@id' => $id]), 108 | '#title_display' => 'invisible', 109 | '#type' => 'textfield', 110 | '#default_value' => isset($options[$id]['alias']) ? $options[$id]['alias'] : '', 111 | '#element_validate' => [[$this, 'validateAliasName']], 112 | ]; 113 | 114 | $form['field_options'][$id]['raw_output'] = [ 115 | '#title' => $this->t('Raw output for @id', ['@id' => $id]), 116 | '#title_display' => 'invisible', 117 | '#type' => 'checkbox', 118 | '#default_value' => isset($options[$id]['raw_output']) ? $options[$id]['raw_output'] : '', 119 | ]; 120 | 121 | $form['field_options'][$id]['type'] = [ 122 | '#type' => 'select', 123 | '#options' => [ 124 | 'String' => $this->t('String'), 125 | 'Int' => $this->t('Int'), 126 | 'Float' => $this->t('Float'), 127 | 'Boolean' => $this->t('Boolean'), 128 | ], 129 | '#default_value' => isset($options[$id]['type']) ? $options[$id]['type'] : 'String', 130 | ]; 131 | } 132 | } 133 | } 134 | 135 | /** 136 | * Form element validation handler. 137 | */ 138 | public function validateAliasName($element, FormStateInterface $form_state) { 139 | if (preg_match('@[^A-Za-z0-9_-]+@', $element['#value'])) { 140 | $form_state->setError($element, $this->t('The machine-readable name must contain only letters, numbers, dashes and underscores.')); 141 | } 142 | } 143 | 144 | /** 145 | * {@inheritdoc} 146 | */ 147 | public function validateOptionsForm(&$form, FormStateInterface $form_state) { 148 | // Collect an array of aliases to validate. 149 | $aliases = static::extractFromOptionsArray('alias', $form_state->getValue(['row_options', 'field_options'])); 150 | 151 | // If array filter returns empty, no values have been entered. Unique keys 152 | // should only be validated if we have some. 153 | if (($filtered = array_filter($aliases)) && (array_unique($filtered) !== $filtered)) { 154 | $form_state->setErrorByName('aliases', $this->t('All field aliases must be unique')); 155 | } 156 | } 157 | 158 | /** 159 | * {@inheritdoc} 160 | */ 161 | public function render($row) { 162 | $output = []; 163 | 164 | foreach ($this->view->field as $id => $field) { 165 | // If the raw output option has been set, just get the raw value. 166 | if (!empty($this->rawOutputOptions[$id])) { 167 | $value = $field->getValue($row); 168 | } 169 | // Otherwise, pass this through the field advancedRender() method. 170 | else { 171 | $value = $field->advancedRender($row); 172 | } 173 | 174 | // Omit excluded fields from the rendered output. 175 | if (empty($field->options['exclude'])) { 176 | $output[$this->getFieldKeyAlias($id)] = $value; 177 | } 178 | } 179 | 180 | return $output; 181 | } 182 | 183 | /** 184 | * Return an alias for a field ID, as set in the options form. 185 | * 186 | * @param string $id 187 | * The field id to lookup an alias for. 188 | * 189 | * @return string 190 | * The matches user entered alias, or the original ID if nothing is found. 191 | */ 192 | public function getFieldKeyAlias($id) { 193 | if (isset($this->replacementAliases[$id])) { 194 | return $this->replacementAliases[$id]; 195 | } 196 | 197 | return $id; 198 | } 199 | 200 | /** 201 | * Return a GraphQL field type, as set in the options form. 202 | * 203 | * @param string $id 204 | * The field id to lookup a type for. 205 | * 206 | * @return string 207 | * The matches user entered type, or String. 208 | */ 209 | public function getFieldType($id) { 210 | if (isset($this->typeOptions[$id])) { 211 | return $this->typeOptions[$id]; 212 | } 213 | 214 | return 'String'; 215 | } 216 | 217 | /** 218 | * Extracts a set of option values from a nested options array. 219 | * 220 | * @param string $key 221 | * The key to extract from each array item. 222 | * @param array $options 223 | * The options array to return values from. 224 | * 225 | * @return array 226 | * A regular one dimensional array of values. 227 | */ 228 | protected static function extractFromOptionsArray($key, array $options) { 229 | return array_map(function($item) use ($key) { 230 | return isset($item[$key]) ? $item[$key] : NULL; 231 | }, $options); 232 | } 233 | 234 | } 235 | -------------------------------------------------------------------------------- /src/Plugin/views/style/GraphQL.php: -------------------------------------------------------------------------------- 1 | view->result as $row_index => $row) { 45 | $this->view->row_index = $row_index; 46 | $rows[] = $this->view->rowPlugin->render($row); 47 | } 48 | unset($this->view->row_index); 49 | 50 | return $rows; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ViewDeriverHelperTrait.php: -------------------------------------------------------------------------------- 1 | [ 33 | 'type' => StringHelper::camelCase($id, 'contextual', 'filter', 'input'), 34 | ], 35 | ]; 36 | } 37 | 38 | return []; 39 | } 40 | 41 | /** 42 | * Helper function to retrieve the sort arguments if any are exposed. 43 | * 44 | * @param \Drupal\views\Plugin\views\display\DisplayPluginInterface $display 45 | * The display plugin. 46 | * @param string $id 47 | * The plugin derivative id. 48 | * 49 | * @return array 50 | * The sort arguments if any exposed sorts are available. 51 | */ 52 | protected function getSortArguments(DisplayPluginInterface $display, $id) { 53 | $sorts = array_filter($display->getOption('sorts') ?: [], function ($sort) { 54 | return $sort['exposed']; 55 | }); 56 | return $sorts ? [ 57 | 'sortDirection' => [ 58 | 'type' => 'ViewSortDirection', 59 | 'default' => 'asc', 60 | ], 61 | 'sortBy' => [ 62 | 'type' => StringHelper::camelCase($id, 'sort', 'by'), 63 | ], 64 | ] : []; 65 | } 66 | 67 | /** 68 | * Helper function to return the filter argument if applicable. 69 | * 70 | * @param \Drupal\views\Plugin\views\display\DisplayPluginInterface $display 71 | * The display plugin. 72 | * @param string $id 73 | * The plugin derivative id. 74 | * 75 | * @return array 76 | * The filter argument if any exposed filters are available. 77 | */ 78 | protected function getFilterArguments(DisplayPluginInterface $display, $id) { 79 | $filters = array_filter($display->getOption('filters') ?: [], function ($filter) { 80 | return array_key_exists('exposed', $filter) && $filter['exposed']; 81 | }); 82 | 83 | return !empty($filters) ? [ 84 | 'filter' => [ 85 | 'type' => $display->getGraphQLFilterInputName(), 86 | ], 87 | ] : []; 88 | } 89 | 90 | /** 91 | * Helper function to retrieve the pager arguments if the display is paged. 92 | * 93 | * @param \Drupal\views\Plugin\views\display\DisplayPluginInterface $display 94 | * The display plugin. 95 | * 96 | * @return array 97 | * An array of pager arguments if the view display is paged. 98 | */ 99 | protected function getPagerArguments(DisplayPluginInterface $display) { 100 | return $this->isPaged($display) ? [ 101 | 'page' => ['type' => 'Int', 'default' => $this->getPagerOffset($display)], 102 | 'pageSize' => [ 103 | 'type' => 'Int', 104 | 'default' => $this->getPagerLimit($display), 105 | ], 106 | ] : []; 107 | } 108 | 109 | /** 110 | * Helper function to retrieve the types that the view can be attached to. 111 | * 112 | * @param array $arguments 113 | * An array containing information about the available arguments. 114 | * @param array $types 115 | * Types where it needs to be added. 116 | * 117 | * @return array 118 | * An array of additional types the view can be embedded in. 119 | */ 120 | protected function getTypes(array $arguments, array $types = ['Root']) { 121 | 122 | if (empty($arguments)) { 123 | return $types; 124 | } 125 | 126 | foreach ($arguments as $argument) { 127 | // Depending on whether bundles are known, we expose the view field 128 | // either on the interface (e.g. Node) or on the type (e.g. NodePage) 129 | // level. Here we specify types managed by other graphql_* modules, 130 | // yet we don't define these modules as dependencies. If types are not 131 | // in the schema, the resulting GraphQL field will be attached to 132 | // nowhere, so it won't go into the schema. 133 | if (empty($argument['bundles']) && empty($argument['entity_type'])) { 134 | continue; 135 | } 136 | 137 | if (empty($argument['bundles'])) { 138 | $types = array_merge($types, [StringHelper::camelCase($argument['entity_type'])]); 139 | } 140 | else { 141 | $types = array_merge($types, array_map(function ($bundle) use ($argument) { 142 | return StringHelper::camelCase($argument['entity_type'], $bundle); 143 | }, array_keys($argument['bundles']))); 144 | } 145 | } 146 | 147 | return $types; 148 | } 149 | 150 | /** 151 | * Check if a pager is configured. 152 | * 153 | * @param \Drupal\views\Plugin\views\display\DisplayPluginInterface $display 154 | * The display configuration. 155 | * 156 | * @return bool 157 | * Flag indicating if the view is configured with a pager. 158 | */ 159 | protected function isPaged(DisplayPluginInterface $display) { 160 | $pagerOptions = $display->getOption('pager'); 161 | return isset($pagerOptions['type']) && in_array($pagerOptions['type'], [ 162 | 'full', 163 | 'mini', 164 | ]); 165 | } 166 | 167 | /** 168 | * Returns a view display object. 169 | * 170 | * @param \Drupal\views\ViewEntityInterface $view 171 | * The view object. 172 | * @param string $displayId 173 | * The display ID to use. 174 | * 175 | * @return \Drupal\views\Plugin\views\display\DisplayPluginInterface 176 | * The view display object. 177 | */ 178 | protected function getViewDisplay(ViewEntityInterface $view, $displayId) { 179 | $viewExecutable = $view->getExecutable(); 180 | $viewExecutable->setDisplay($displayId); 181 | return $viewExecutable->getDisplay(); 182 | } 183 | 184 | /** 185 | * Get the configured default limit. 186 | * 187 | * @param \Drupal\views\Plugin\views\display\DisplayPluginInterface $display 188 | * The display configuration. 189 | * 190 | * @return int 191 | * The default limit. 192 | */ 193 | protected function getPagerLimit(DisplayPluginInterface $display) { 194 | $pagerOptions = $display->getOption('pager'); 195 | return NestedArray::getValue($pagerOptions, [ 196 | 'options', 197 | 'items_per_page', 198 | ]) ?: 0; 199 | } 200 | 201 | /** 202 | * Get the configured default offset. 203 | * 204 | * @param \Drupal\views\Plugin\views\display\DisplayPluginInterface $display 205 | * The display configuration. 206 | * 207 | * @return int 208 | * The default offset. 209 | */ 210 | protected function getPagerOffset(DisplayPluginInterface $display) { 211 | $pagerOptions = $display->getOption('pager'); 212 | return NestedArray::getValue($pagerOptions, [ 213 | 'options', 214 | 'offset', 215 | ]) ?: 0; 216 | } 217 | 218 | /** 219 | * Check if a certain interface exists. 220 | * 221 | * @param string $interface 222 | * The GraphQL interface name. 223 | * @param \Drupal\Component\Plugin\PluginManagerInterface $interfacePluginManager 224 | * Plugin interface manager. 225 | * 226 | * @return bool 227 | * Boolean flag indicating if the interface exists. 228 | */ 229 | protected function interfaceExists($interface, PluginManagerInterface $interfacePluginManager) { 230 | return (bool) array_filter($interfacePluginManager->getDefinitions(), function ($definition) use ($interface) { 231 | return $definition['name'] === $interface; 232 | }); 233 | } 234 | 235 | /** 236 | * Retrieves the type the view's rows resolve to. 237 | * 238 | * @param \Drupal\views\ViewEntityInterface $view 239 | * The view entity. 240 | * @param string $displayId 241 | * The id of the current display. 242 | * @param \Drupal\Component\Plugin\PluginManagerInterface $interfacePluginManager 243 | * Interface plugin manager. 244 | * 245 | * @return null|string 246 | * The name of the type or NULL if the type could not be derived. 247 | */ 248 | protected function getRowResolveType(ViewEntityInterface $view, $displayId, PluginManagerInterface $interfacePluginManager) { 249 | /** @var \Drupal\graphql_views\Plugin\views\display\GraphQL $display */ 250 | $display = $this->getViewDisplay($view, $displayId); 251 | $rowPlugin = $display->getPlugin('row'); 252 | 253 | if ($rowPlugin instanceof GraphQLFieldRow) { 254 | return StringHelper::camelCase($display->getGraphQLRowName()); 255 | } 256 | 257 | if ($rowPlugin instanceof GraphQLEntityRow) { 258 | $executable = $view->getExecutable(); 259 | $executable->setDisplay($displayId); 260 | 261 | if ($entityType = $executable->getBaseEntityType()) { 262 | $typeName = $entityType->id(); 263 | $typeNameCamel = StringHelper::camelCase($typeName); 264 | if ($this->interfaceExists($typeNameCamel, $interfacePluginManager)) { 265 | $filters = $executable->getDisplay()->getOption('filters'); 266 | $dataTable = $entityType->getDataTable(); 267 | $bundleKey = $entityType->getKey('bundle'); 268 | 269 | foreach ($filters as $filter) { 270 | $isBundleFilter = $filter['table'] == $dataTable && $filter['field'] == $bundleKey; 271 | $isSingleValued = is_array($filter['value']) && count($filter['value']) == 1; 272 | $isExposed = isset($filter['exposed']) && $filter['exposed']; 273 | if ($isBundleFilter && $isSingleValued && !$isExposed) { 274 | $bundle = reset($filter['value']); 275 | $typeName .= "_$bundle"; 276 | break; 277 | } 278 | } 279 | 280 | return StringHelper::camelCase($typeName); 281 | } 282 | } 283 | 284 | return 'Entity'; 285 | } 286 | 287 | return NULL; 288 | } 289 | 290 | /** 291 | * Returns a view style object. 292 | * 293 | * @param \Drupal\views\ViewEntityInterface $view 294 | * The view object. 295 | * @param string $displayId 296 | * The display ID to use. 297 | * 298 | * @return \Drupal\views\Plugin\views\style\StylePluginBase 299 | * The view style object. 300 | */ 301 | protected function getViewStyle(ViewEntityInterface $view, $displayId) { 302 | $viewExecutable = $view->getExecutable(); 303 | $viewExecutable->setDisplay($displayId); 304 | return $viewExecutable->getStyle(); 305 | } 306 | 307 | /** 308 | * Returns cache metadata plugin definitions. 309 | * 310 | * @param \Drupal\views\ViewEntityInterface $view 311 | * The view object. 312 | * @param \Drupal\views\Plugin\views\display\DisplayPluginInterface $display 313 | * The view display. 314 | * 315 | * @return array 316 | * The cache metadata definitions for the plugin definition. 317 | */ 318 | protected function getCacheMetadataDefinition(ViewEntityInterface $view, DisplayPluginInterface $display) { 319 | $metadata = $display->getCacheMetadata() 320 | ->addCacheTags($view->getCacheTags()) 321 | ->addCacheContexts($view->getCacheContexts()) 322 | ->mergeCacheMaxAge($view->getCacheMaxAge()); 323 | 324 | return [ 325 | 'schema_cache_tags' => $metadata->getCacheTags(), 326 | 'schema_cache_max_age' => $metadata->getCacheMaxAge(), 327 | 'response_cache_contexts' => array_filter($metadata->getCacheContexts(), function ($context) { 328 | // Don't emit the url cache contexts. 329 | return $context !== 'url' && strpos($context, 'url.') !== 0; 330 | }), 331 | ]; 332 | } 333 | 334 | /** 335 | * Returns information about view arguments (contextual filters). 336 | * 337 | * @param array $viewArguments 338 | * The "arguments" option of a view display. 339 | * 340 | * @return array 341 | * Arguments information keyed by the argument ID. Subsequent array keys: 342 | * - index: argument index. 343 | * - entity_type: target entity type. 344 | * - bundles: target bundles (can be empty). 345 | * 346 | * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException 347 | */ 348 | protected function getArgumentsInfo(array $viewArguments) { 349 | $argumentsInfo = []; 350 | /* @var \Drupal\Core\Entity\EntityTypeManager $entityTypeManager */ 351 | $entityTypeManager = \Drupal::service('entity_type.manager'); 352 | 353 | $index = 0; 354 | foreach ($viewArguments as $argumentId => $argument) { 355 | $info = [ 356 | 'index' => $index, 357 | 'entity_type' => NULL, 358 | 'bundles' => [], 359 | ]; 360 | 361 | if (isset($argument['entity_type']) && isset($argument['entity_field'])) { 362 | $entityType = $entityTypeManager->getDefinition($argument['entity_type']); 363 | if ($entityType) { 364 | $idField = $entityType->getKey('id'); 365 | if ($idField === $argument['entity_field']) { 366 | $info['entity_type'] = $argument['entity_type']; 367 | if ( 368 | $argument['specify_validation'] && 369 | strpos($argument['validate']['type'], 'entity:') === 0 && 370 | !empty($argument['validate_options']['bundles']) 371 | ) { 372 | $info['bundles'] = $argument['validate_options']['bundles']; 373 | } 374 | } 375 | } 376 | } 377 | 378 | $argumentsInfo[$argumentId] = $info; 379 | $index++; 380 | } 381 | 382 | return $argumentsInfo; 383 | } 384 | 385 | } 386 | -------------------------------------------------------------------------------- /tests/modules/graphql_views_test/config/install/views.view.graphql_bundle_test.yml: -------------------------------------------------------------------------------- 1 | uuid: 8abe765b-8d05-4b97-8a39-0be2ac93f439 2 | langcode: en 3 | status: true 4 | dependencies: 5 | config: 6 | - node.type.test 7 | module: 8 | - graphql 9 | - node 10 | - user 11 | id: graphql_bundle_test 12 | label: 'GraphQL Bundle Test' 13 | module: views 14 | description: '' 15 | tag: '' 16 | base_table: node_field_data 17 | base_field: nid 18 | core: 8.x 19 | core_version_requirement: ^8 || ^9 20 | display: 21 | default: 22 | display_plugin: default 23 | id: default 24 | display_title: Master 25 | position: 0 26 | display_options: 27 | access: 28 | type: perm 29 | options: 30 | perm: 'access content' 31 | cache: 32 | type: tag 33 | options: { } 34 | query: 35 | type: views_query 36 | options: 37 | disable_sql_rewrite: false 38 | distinct: false 39 | replica: false 40 | query_comment: '' 41 | query_tags: { } 42 | exposed_form: 43 | type: basic 44 | options: 45 | submit_button: Apply 46 | reset_button: false 47 | reset_button_label: Reset 48 | exposed_sorts_label: 'Sort by' 49 | expose_sort_order: true 50 | sort_asc_label: Asc 51 | sort_desc_label: Desc 52 | pager: 53 | type: mini 54 | options: 55 | items_per_page: 10 56 | offset: 0 57 | id: 0 58 | total_pages: null 59 | expose: 60 | items_per_page: false 61 | items_per_page_label: 'Items per page' 62 | items_per_page_options: '5, 10, 25, 50' 63 | items_per_page_options_all: false 64 | items_per_page_options_all_label: '- All -' 65 | offset: false 66 | offset_label: Offset 67 | tags: 68 | previous: ‹‹ 69 | next: ›› 70 | style: 71 | type: default 72 | options: 73 | grouping: { } 74 | row_class: '' 75 | default_row_class: true 76 | uses_fields: false 77 | row: 78 | type: fields 79 | options: 80 | inline: { } 81 | separator: '' 82 | hide_empty: false 83 | default_field_elements: true 84 | fields: 85 | title: 86 | id: title 87 | table: node_field_data 88 | field: title 89 | entity_type: node 90 | entity_field: title 91 | label: '' 92 | alter: 93 | alter_text: false 94 | make_link: false 95 | absolute: false 96 | trim: false 97 | word_boundary: false 98 | ellipsis: false 99 | strip_tags: false 100 | html: false 101 | hide_empty: false 102 | empty_zero: false 103 | settings: 104 | link_to_entity: true 105 | plugin_id: field 106 | relationship: none 107 | group_type: group 108 | admin_label: '' 109 | exclude: false 110 | element_type: '' 111 | element_class: '' 112 | element_label_type: '' 113 | element_label_class: '' 114 | element_label_colon: true 115 | element_wrapper_type: '' 116 | element_wrapper_class: '' 117 | element_default_classes: true 118 | empty: '' 119 | hide_alter_empty: true 120 | click_sort_column: value 121 | type: string 122 | group_column: value 123 | group_columns: { } 124 | group_rows: true 125 | delta_limit: 0 126 | delta_offset: 0 127 | delta_reversed: false 128 | delta_first_last: false 129 | multi_type: separator 130 | separator: ', ' 131 | field_api_classes: false 132 | filters: 133 | status: 134 | value: '1' 135 | table: node_field_data 136 | field: status 137 | plugin_id: boolean 138 | entity_type: node 139 | entity_field: status 140 | id: status 141 | expose: 142 | operator: '' 143 | group: 1 144 | type: 145 | id: type 146 | table: node_field_data 147 | field: type 148 | value: 149 | test: test 150 | entity_type: node 151 | entity_field: type 152 | plugin_id: bundle 153 | sorts: 154 | nid: 155 | id: nid 156 | table: node_field_data 157 | field: nid 158 | relationship: none 159 | group_type: group 160 | admin_label: '' 161 | order: ASC 162 | exposed: false 163 | expose: 164 | label: '' 165 | entity_type: node 166 | entity_field: nid 167 | plugin_id: standard 168 | 169 | header: { } 170 | footer: { } 171 | empty: { } 172 | relationships: { } 173 | arguments: { } 174 | display_extenders: { } 175 | cache_metadata: 176 | max-age: -1 177 | contexts: 178 | - 'languages:language_content' 179 | - 'languages:language_interface' 180 | - url.query_args 181 | - 'user.node_grants:view' 182 | - user.permissions 183 | tags: { } 184 | graphql_1: 185 | display_plugin: graphql 186 | id: graphql_1 187 | display_title: GraphQL 188 | position: 1 189 | display_options: 190 | display_extenders: { } 191 | pager: 192 | type: some 193 | options: 194 | items_per_page: 1 195 | offset: 0 196 | cache_metadata: 197 | max-age: -1 198 | contexts: 199 | - 'languages:language_content' 200 | - 'languages:language_interface' 201 | - 'user.node_grants:view' 202 | - user.permissions 203 | tags: { } 204 | -------------------------------------------------------------------------------- /tests/modules/graphql_views_test/config/install/views.view.graphql_test.yml: -------------------------------------------------------------------------------- 1 | uuid: 287a1f02-3fd1-4b5b-8c77-76855fa7f308 2 | langcode: en 3 | status: true 4 | dependencies: 5 | config: 6 | - taxonomy.vocabulary.tags 7 | module: 8 | - graphql 9 | - node 10 | - taxonomy 11 | - user 12 | id: graphql_test 13 | label: 'GraphQL Test' 14 | module: views 15 | description: 'Views configurations for GraphQL integration testing.' 16 | tag: '' 17 | base_table: node_field_data 18 | base_field: nid 19 | core: 8.x 20 | core_version_requirement: ^8 || ^9 21 | display: 22 | default: 23 | display_plugin: default 24 | id: default 25 | display_title: Master 26 | position: 0 27 | display_options: 28 | access: 29 | type: perm 30 | options: 31 | perm: 'access content' 32 | cache: 33 | type: tag 34 | options: { } 35 | query: 36 | type: views_query 37 | options: 38 | disable_sql_rewrite: false 39 | distinct: false 40 | replica: false 41 | query_comment: '' 42 | query_tags: { } 43 | exposed_form: 44 | type: basic 45 | options: 46 | submit_button: Apply 47 | reset_button: false 48 | reset_button_label: Reset 49 | exposed_sorts_label: 'Sort by' 50 | expose_sort_order: true 51 | sort_asc_label: Asc 52 | sort_desc_label: Desc 53 | pager: 54 | type: mini 55 | options: 56 | items_per_page: 10 57 | offset: 0 58 | id: 0 59 | total_pages: null 60 | expose: 61 | items_per_page: false 62 | items_per_page_label: 'Items per page' 63 | items_per_page_options: '5, 10, 25, 50' 64 | items_per_page_options_all: false 65 | items_per_page_options_all_label: '- All -' 66 | offset: false 67 | offset_label: Offset 68 | tags: 69 | previous: ‹‹ 70 | next: ›› 71 | style: 72 | type: default 73 | options: 74 | grouping: { } 75 | row_class: '' 76 | default_row_class: true 77 | uses_fields: false 78 | row: 79 | type: graphql_entity 80 | options: { } 81 | fields: 82 | title: 83 | id: title 84 | table: node_field_data 85 | field: title 86 | entity_type: node 87 | entity_field: title 88 | label: '' 89 | alter: 90 | alter_text: false 91 | make_link: false 92 | absolute: false 93 | trim: false 94 | word_boundary: false 95 | ellipsis: false 96 | strip_tags: false 97 | html: false 98 | hide_empty: false 99 | empty_zero: false 100 | settings: 101 | link_to_entity: true 102 | plugin_id: field 103 | relationship: none 104 | group_type: group 105 | admin_label: '' 106 | exclude: false 107 | element_type: '' 108 | element_class: '' 109 | element_label_type: '' 110 | element_label_class: '' 111 | element_label_colon: true 112 | element_wrapper_type: '' 113 | element_wrapper_class: '' 114 | element_default_classes: true 115 | empty: '' 116 | hide_alter_empty: true 117 | click_sort_column: value 118 | type: string 119 | group_column: value 120 | group_columns: { } 121 | group_rows: true 122 | delta_limit: 0 123 | delta_offset: 0 124 | delta_reversed: false 125 | delta_first_last: false 126 | multi_type: separator 127 | separator: ', ' 128 | field_api_classes: false 129 | filters: 130 | title: 131 | id: title 132 | table: node_field_data 133 | field: title 134 | relationship: none 135 | group_type: group 136 | admin_label: '' 137 | operator: contains 138 | value: '' 139 | group: 1 140 | exposed: true 141 | expose: 142 | operator_id: title_op 143 | label: Title 144 | description: '' 145 | use_operator: false 146 | operator: title_op 147 | identifier: title 148 | required: false 149 | remember: false 150 | multiple: false 151 | remember_roles: 152 | authenticated: authenticated 153 | anonymous: '0' 154 | administrator: '0' 155 | is_grouped: false 156 | group_info: 157 | label: '' 158 | description: '' 159 | identifier: '' 160 | optional: true 161 | widget: select 162 | multiple: false 163 | remember: false 164 | default_group: All 165 | default_group_multiple: { } 166 | group_items: { } 167 | entity_type: node 168 | entity_field: title 169 | plugin_id: string 170 | field_tags_target_id: 171 | id: field_tags_target_id 172 | table: node__field_tags 173 | field: field_tags_target_id 174 | relationship: none 175 | group_type: group 176 | admin_label: Tags 177 | operator: or 178 | value: { } 179 | group: 1 180 | exposed: true 181 | expose: 182 | operator_id: field_tags_target_id_op 183 | label: Tags 184 | description: '' 185 | use_operator: false 186 | operator: field_tags_target_id_op 187 | identifier: field_tags 188 | required: false 189 | remember: false 190 | multiple: true 191 | remember_roles: 192 | authenticated: authenticated 193 | anonymous: '0' 194 | administrator: '0' 195 | reduce: false 196 | is_grouped: false 197 | group_info: 198 | label: '' 199 | description: '' 200 | identifier: '' 201 | optional: true 202 | widget: select 203 | multiple: false 204 | remember: false 205 | default_group: All 206 | default_group_multiple: { } 207 | group_items: { } 208 | reduce_duplicates: false 209 | type: select 210 | limit: true 211 | vid: tags 212 | hierarchy: false 213 | error_message: true 214 | plugin_id: taxonomy_index_tid 215 | type: 216 | id: type 217 | table: node_field_data 218 | field: type 219 | relationship: none 220 | group_type: group 221 | admin_label: '' 222 | operator: in 223 | value: 224 | test: test 225 | group: 1 226 | exposed: true 227 | expose: 228 | operator_id: type_op 229 | label: 'Content type' 230 | description: '' 231 | use_operator: false 232 | operator: type_op 233 | identifier: node_type 234 | required: false 235 | remember: false 236 | multiple: false 237 | remember_roles: 238 | authenticated: authenticated 239 | anonymous: '0' 240 | administrator: '0' 241 | reduce: true 242 | is_grouped: false 243 | group_info: 244 | label: '' 245 | description: '' 246 | identifier: '' 247 | optional: true 248 | widget: select 249 | multiple: false 250 | remember: false 251 | default_group: All 252 | default_group_multiple: { } 253 | group_items: { } 254 | entity_type: node 255 | entity_field: type 256 | plugin_id: bundle 257 | filter_groups: 258 | operator: AND 259 | groups: 260 | 1: AND 261 | sorts: 262 | nid: 263 | id: nid 264 | table: node_field_data 265 | field: nid 266 | relationship: none 267 | group_type: group 268 | admin_label: '' 269 | order: ASC 270 | exposed: false 271 | expose: 272 | label: '' 273 | entity_type: node 274 | entity_field: nid 275 | plugin_id: standard 276 | header: { } 277 | footer: { } 278 | empty: { } 279 | relationships: { } 280 | arguments: { } 281 | display_extenders: { } 282 | cache_metadata: 283 | max-age: -1 284 | contexts: 285 | - 'languages:language_content' 286 | - 'languages:language_interface' 287 | - url 288 | - url.query_args 289 | - user 290 | - 'user.node_grants:view' 291 | - user.permissions 292 | tags: { } 293 | contextual_node: 294 | display_plugin: graphql 295 | id: contextual_node 296 | display_title: 'Contextual: Node' 297 | position: 6 298 | display_options: 299 | display_extenders: { } 300 | display_description: '' 301 | arguments: 302 | nid: 303 | id: nid 304 | table: node_field_data 305 | field: nid 306 | relationship: none 307 | group_type: group 308 | admin_label: '' 309 | default_action: ignore 310 | exception: 311 | value: all 312 | title_enable: false 313 | title: All 314 | title_enable: false 315 | title: '' 316 | default_argument_type: fixed 317 | default_argument_options: 318 | argument: '' 319 | default_argument_skip_url: false 320 | summary_options: 321 | base_path: '' 322 | count: true 323 | items_per_page: 25 324 | override: false 325 | summary: 326 | sort_order: asc 327 | number_of_records: 0 328 | format: default_summary 329 | specify_validation: false 330 | validate: 331 | type: none 332 | fail: 'not found' 333 | validate_options: { } 334 | break_phrase: false 335 | not: false 336 | entity_type: node 337 | entity_field: nid 338 | plugin_id: node_nid 339 | defaults: 340 | arguments: false 341 | cache_metadata: 342 | max-age: -1 343 | contexts: 344 | - 'languages:language_content' 345 | - 'languages:language_interface' 346 | - url 347 | - user 348 | - 'user.node_grants:view' 349 | - user.permissions 350 | tags: { } 351 | contextual_node_and_nodetest: 352 | display_plugin: graphql 353 | id: contextual_node_and_nodetest 354 | display_title: 'Contextual: Node and NodeTest' 355 | position: 8 356 | display_options: 357 | display_extenders: { } 358 | display_description: '' 359 | arguments: 360 | nid: 361 | id: nid 362 | table: node_field_data 363 | field: nid 364 | relationship: none 365 | group_type: group 366 | admin_label: '' 367 | default_action: ignore 368 | exception: 369 | value: all 370 | title_enable: false 371 | title: All 372 | title_enable: false 373 | title: '' 374 | default_argument_type: fixed 375 | default_argument_options: 376 | argument: '' 377 | default_argument_skip_url: false 378 | summary_options: 379 | base_path: '' 380 | count: true 381 | items_per_page: 25 382 | override: false 383 | summary: 384 | sort_order: asc 385 | number_of_records: 0 386 | format: default_summary 387 | specify_validation: false 388 | validate: 389 | type: none 390 | fail: 'not found' 391 | validate_options: { } 392 | break_phrase: false 393 | not: false 394 | entity_type: node 395 | entity_field: nid 396 | plugin_id: node_nid 397 | nid_1: 398 | id: nid_1 399 | table: node_field_data 400 | field: nid 401 | relationship: none 402 | group_type: group 403 | admin_label: '' 404 | default_action: ignore 405 | exception: 406 | value: all 407 | title_enable: false 408 | title: All 409 | title_enable: false 410 | title: '' 411 | default_argument_type: fixed 412 | default_argument_options: 413 | argument: '' 414 | default_argument_skip_url: false 415 | summary_options: 416 | base_path: '' 417 | count: true 418 | items_per_page: 25 419 | override: false 420 | summary: 421 | sort_order: asc 422 | number_of_records: 0 423 | format: default_summary 424 | specify_validation: true 425 | validate: 426 | type: 'entity:node' 427 | fail: 'not found' 428 | validate_options: 429 | bundles: 430 | test: test 431 | operation: view 432 | multiple: 0 433 | access: false 434 | break_phrase: false 435 | not: false 436 | entity_type: node 437 | entity_field: nid 438 | plugin_id: node_nid 439 | defaults: 440 | arguments: false 441 | cache_metadata: 442 | max-age: -1 443 | contexts: 444 | - 'languages:language_content' 445 | - 'languages:language_interface' 446 | - url 447 | - user 448 | - 'user.node_grants:view' 449 | - user.permissions 450 | tags: { } 451 | contextual_nodetest: 452 | display_plugin: graphql 453 | id: contextual_nodetest 454 | display_title: 'Contextual: NodeTest' 455 | position: 7 456 | display_options: 457 | display_extenders: { } 458 | display_description: '' 459 | arguments: 460 | nid: 461 | id: nid 462 | table: node_field_data 463 | field: nid 464 | relationship: none 465 | group_type: group 466 | admin_label: '' 467 | default_action: ignore 468 | exception: 469 | value: all 470 | title_enable: false 471 | title: All 472 | title_enable: false 473 | title: '' 474 | default_argument_type: fixed 475 | default_argument_options: 476 | argument: '' 477 | default_argument_skip_url: false 478 | summary_options: 479 | base_path: '' 480 | count: true 481 | items_per_page: 25 482 | override: false 483 | summary: 484 | sort_order: asc 485 | number_of_records: 0 486 | format: default_summary 487 | specify_validation: true 488 | validate: 489 | type: 'entity:node' 490 | fail: 'not found' 491 | validate_options: 492 | bundles: 493 | test: test 494 | operation: view 495 | multiple: 0 496 | access: false 497 | break_phrase: false 498 | not: false 499 | entity_type: node 500 | entity_field: nid 501 | plugin_id: node_nid 502 | defaults: 503 | arguments: false 504 | cache_metadata: 505 | max-age: -1 506 | contexts: 507 | - 'languages:language_content' 508 | - 'languages:language_interface' 509 | - url 510 | - user 511 | - 'user.node_grants:view' 512 | - user.permissions 513 | tags: { } 514 | contextual_title_arg: 515 | display_plugin: graphql 516 | id: contextual_title_arg 517 | display_title: 'Contextual: title arg' 518 | position: 5 519 | display_options: 520 | display_extenders: { } 521 | arguments: 522 | title: 523 | id: title 524 | table: node_field_data 525 | field: title 526 | relationship: none 527 | group_type: group 528 | admin_label: '' 529 | default_action: ignore 530 | exception: 531 | value: all 532 | title_enable: false 533 | title: All 534 | title_enable: false 535 | title: '' 536 | default_argument_type: fixed 537 | default_argument_options: 538 | argument: '' 539 | default_argument_skip_url: false 540 | summary_options: 541 | base_path: '' 542 | count: true 543 | items_per_page: 25 544 | override: false 545 | summary: 546 | sort_order: asc 547 | number_of_records: 0 548 | format: default_summary 549 | specify_validation: false 550 | validate: 551 | type: none 552 | fail: 'not found' 553 | validate_options: { } 554 | glossary: false 555 | limit: 0 556 | case: none 557 | path_case: none 558 | transform_dash: false 559 | break_phrase: false 560 | entity_type: node 561 | entity_field: title 562 | plugin_id: string 563 | defaults: 564 | arguments: false 565 | display_description: '' 566 | cache_metadata: 567 | max-age: -1 568 | contexts: 569 | - 'languages:language_content' 570 | - 'languages:language_interface' 571 | - url 572 | - user 573 | - 'user.node_grants:view' 574 | - user.permissions 575 | tags: { } 576 | filtered: 577 | display_plugin: graphql 578 | id: filtered 579 | display_title: Filtered 580 | position: 4 581 | display_options: 582 | display_extenders: { } 583 | display_description: '' 584 | filters: { } 585 | defaults: 586 | filters: true 587 | filter_groups: true 588 | pager: 589 | type: none 590 | options: 591 | offset: 0 592 | cache_metadata: 593 | max-age: -1 594 | contexts: 595 | - 'languages:language_content' 596 | - 'languages:language_interface' 597 | - url 598 | - user 599 | - 'user.node_grants:view' 600 | - user.permissions 601 | tags: { } 602 | paged: 603 | display_plugin: graphql 604 | id: paged 605 | display_title: Paged 606 | position: 2 607 | display_options: 608 | display_extenders: { } 609 | display_description: '' 610 | pager: 611 | type: full 612 | options: 613 | items_per_page: 2 614 | offset: 0 615 | id: 0 616 | total_pages: null 617 | tags: 618 | previous: '‹ Previous' 619 | next: 'Next ›' 620 | first: '« First' 621 | last: 'Last »' 622 | expose: 623 | items_per_page: false 624 | items_per_page_label: 'Items per page' 625 | items_per_page_options: '5, 10, 25, 50' 626 | items_per_page_options_all: false 627 | items_per_page_options_all_label: '- All -' 628 | offset: false 629 | offset_label: Offset 630 | quantity: 9 631 | cache_metadata: 632 | max-age: -1 633 | contexts: 634 | - 'languages:language_content' 635 | - 'languages:language_interface' 636 | - url 637 | - url.query_args 638 | - user 639 | - 'user.node_grants:view' 640 | - user.permissions 641 | tags: { } 642 | simple: 643 | display_plugin: graphql 644 | id: simple 645 | display_title: Simple 646 | position: 1 647 | display_options: 648 | display_extenders: { } 649 | display_description: '' 650 | pager: 651 | type: some 652 | options: 653 | items_per_page: 3 654 | offset: 0 655 | cache_metadata: 656 | max-age: -1 657 | contexts: 658 | - 'languages:language_content' 659 | - 'languages:language_interface' 660 | - url 661 | - user 662 | - 'user.node_grants:view' 663 | - user.permissions 664 | tags: { } 665 | sorted: 666 | display_plugin: graphql 667 | id: sorted 668 | display_title: Sorted 669 | position: 3 670 | display_options: 671 | display_extenders: { } 672 | display_description: '' 673 | sorts: 674 | nid: 675 | id: nid 676 | table: node_field_data 677 | field: nid 678 | relationship: none 679 | group_type: group 680 | admin_label: '' 681 | order: ASC 682 | exposed: true 683 | expose: 684 | label: ID 685 | entity_type: node 686 | entity_field: nid 687 | plugin_id: standard 688 | title: 689 | id: title 690 | table: node_field_data 691 | field: title 692 | relationship: none 693 | group_type: group 694 | admin_label: '' 695 | order: ASC 696 | exposed: true 697 | expose: 698 | label: Title 699 | entity_type: node 700 | entity_field: title 701 | plugin_id: standard 702 | defaults: 703 | sorts: false 704 | pager: 705 | type: some 706 | options: 707 | items_per_page: 3 708 | offset: 0 709 | cache_metadata: 710 | max-age: -1 711 | contexts: 712 | - 'languages:language_content' 713 | - 'languages:language_interface' 714 | - url 715 | - 'url.query_args:sort_by' 716 | - 'url.query_args:sort_order' 717 | - user 718 | - 'user.node_grants:view' 719 | - user.permissions 720 | tags: { } 721 | -------------------------------------------------------------------------------- /tests/modules/graphql_views_test/graphql_views_test.features.yml: -------------------------------------------------------------------------------- 1 | required: true 2 | -------------------------------------------------------------------------------- /tests/modules/graphql_views_test/graphql_views_test.info.yml: -------------------------------------------------------------------------------- 1 | name: 'GraphQL Views Test' 2 | description: 'Test configurations for GraphQL views integration.' 3 | type: module 4 | core: 8.x 5 | core_version_requirement: ^8 || ^9 6 | dependencies: 7 | - graphql_views 8 | - graphql_content_test 9 | - node 10 | - taxonomy 11 | - user 12 | - views 13 | -------------------------------------------------------------------------------- /tests/modules/graphql_views_test/graphql_views_test.module: -------------------------------------------------------------------------------- 1 | storage->id() . ':' . $view->current_display; 8 | if (!isset($args[$id])) { 9 | $args[$id] = []; 10 | } 11 | $args[$id][] = $view->args; 12 | } 13 | -------------------------------------------------------------------------------- /tests/queries/contextual.gql: -------------------------------------------------------------------------------- 1 | query ($test2NodeId: String!) { 2 | 3 | # graphql_test:contextual_title_arg 4 | title_arg0:graphqlTestContextualTitleArgView { 5 | results { 6 | entityId 7 | } 8 | } 9 | 10 | title_arg1:graphqlTestContextualTitleArgView (contextualFilter: {title: "X"}) { 11 | results { 12 | entityId 13 | } 14 | } 15 | 16 | # graphql_test:contextual_node 17 | node0:graphqlTestContextualNodeView { 18 | results { 19 | entityId 20 | } 21 | } 22 | 23 | node1:graphqlTestContextualNodeView (contextualFilter: {nid: "X"}) { 24 | results { 25 | entityId 26 | } 27 | } 28 | 29 | node2:nodeById (id: "1", language: EN) { 30 | ... on Node { 31 | graphqlTestContextualNodeView { 32 | results { 33 | entityId 34 | } 35 | } 36 | } 37 | } 38 | node3:nodeById (id: "1", language: EN) { 39 | ... on Node { 40 | graphqlTestContextualNodeView (contextualFilter: {nid: "X"}) { 41 | results { 42 | entityId 43 | } 44 | } 45 | } 46 | } 47 | node4:nodeById (id: "1", language: EN) { 48 | ... on NodeTest { 49 | graphqlTestContextualNodeView { 50 | results { 51 | entityId 52 | } 53 | } 54 | } 55 | } 56 | node5:nodeById (id: "1", language: EN) { 57 | ... on NodeTest { 58 | graphqlTestContextualNodeView (contextualFilter: {nid: "X"}) { 59 | results { 60 | entityId 61 | } 62 | } 63 | } 64 | } 65 | 66 | # graphql_test:contextual_nodetest 67 | nodetest0:graphqlTestContextualNodetestView { 68 | results { 69 | entityId 70 | } 71 | } 72 | 73 | nodetest1:graphqlTestContextualNodetestView (contextualFilter: {nid: "X"}) { 74 | results { 75 | entityId 76 | } 77 | } 78 | 79 | nodetest2:nodeById (id: "1", language: EN) { 80 | ... on NodeTest { 81 | graphqlTestContextualNodetestView { 82 | results { 83 | entityId 84 | } 85 | } 86 | } 87 | } 88 | 89 | nodetest3:nodeById (id: "1", language: EN) { 90 | ... on NodeTest { 91 | graphqlTestContextualNodetestView (contextualFilter: {nid: "X"}) { 92 | results { 93 | entityId 94 | } 95 | } 96 | } 97 | } 98 | 99 | # graphql_test:contextual_node_and_nodetest 100 | node_and_nodetest0:graphqlTestContextualNodeAndNodetestView { 101 | results { 102 | entityId 103 | } 104 | } 105 | 106 | node_and_nodetest1:graphqlTestContextualNodeAndNodetestView (contextualFilter: {nid: "X", nid_1: "X"}) { 107 | results { 108 | entityId 109 | } 110 | } 111 | 112 | node_and_nodetest2:nodeById (id: $test2NodeId, language: EN) { 113 | ... on Node { 114 | graphqlTestContextualNodeAndNodetestView { 115 | results { 116 | entityId 117 | } 118 | } 119 | } 120 | } 121 | node_and_nodetest3:nodeById (id: $test2NodeId, language: EN) { 122 | ... on Node { 123 | graphqlTestContextualNodeAndNodetestView (contextualFilter: {nid: "X", nid_1: "X"}) { 124 | results { 125 | entityId 126 | } 127 | } 128 | } 129 | } 130 | 131 | node_and_nodetest4:nodeById (id: "1", language: EN) { 132 | ... on NodeTest { 133 | graphqlTestContextualNodeAndNodetestView { 134 | results { 135 | entityId 136 | } 137 | } 138 | } 139 | } 140 | 141 | node_and_nodetest5:nodeById (id: "1", language: EN) { 142 | ... on NodeTest { 143 | graphqlTestContextualNodeAndNodetestView (contextualFilter: {nid: "X", nid_1: "X"}) { 144 | results { 145 | entityId 146 | } 147 | } 148 | } 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /tests/queries/paged.gql: -------------------------------------------------------------------------------- 1 | { 2 | page_one:graphqlTestPagedView { 3 | count 4 | results { 5 | entityLabel 6 | } 7 | } 8 | 9 | page_two:graphqlTestPagedView(page: 1) { 10 | count 11 | results { 12 | entityLabel 13 | } 14 | } 15 | 16 | page_three:graphqlTestPagedView(page: 2, pageSize: 3) { 17 | count 18 | results { 19 | entityLabel 20 | } 21 | } 22 | 23 | page_four:graphqlTestPagedView(page: 2 pageSize: 4) { 24 | count 25 | results { 26 | entityLabel 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /tests/queries/simple.gql: -------------------------------------------------------------------------------- 1 | { 2 | graphqlTestSimpleView { 3 | results { 4 | entityLabel 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /tests/queries/single_bundle_filter.gql: -------------------------------------------------------------------------------- 1 | { 2 | withSingleBundleFilter: graphqlBundleTestGraphql1View { 3 | results { 4 | __typename 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/queries/sorted.gql: -------------------------------------------------------------------------------- 1 | { 2 | default:graphqlTestSortedView { 3 | results { 4 | entityLabel 5 | } 6 | } 7 | 8 | asc:graphqlTestSortedView(sortBy: TITLE) { 9 | results { 10 | entityLabel 11 | } 12 | } 13 | 14 | desc:graphqlTestSortedView(sortBy: TITLE, sortDirection: DESC) { 15 | results { 16 | entityLabel 17 | } 18 | } 19 | 20 | asc_nid:graphqlTestSortedView(sortBy: NID, sortDirection: ASC) { 21 | results { 22 | entityLabel 23 | } 24 | } 25 | 26 | desc_nid:graphqlTestSortedView(sortBy: NID, sortDirection: DESC) { 27 | results { 28 | entityLabel 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/src/Kernel/ContextualViewsTest.php: -------------------------------------------------------------------------------- 1 | createContentType(['type' => 'test2']); 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | protected function defaultCacheContexts() { 26 | return array_merge([ 27 | 'languages:language_content', 28 | 'languages:language_interface', 29 | 'user.permissions', 30 | 'user.node_grants:view', 31 | ], parent::defaultCacheContexts()); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function defaultCacheTags() { 38 | return array_merge([ 39 | 'config:field.storage.node.field_tags', 40 | ], parent::defaultCacheTags()); 41 | } 42 | 43 | /** 44 | * Test if view contextual filters are set properly. 45 | */ 46 | public function testContextualViewArgs() { 47 | $test2Node = $this->createNode(['type' => 'test2']); 48 | 49 | $this->graphQlProcessor()->processQuery( 50 | $this->getDefaultSchema(), 51 | OperationParams::create([ 52 | 'query' => $this->getQueryFromFile('contextual.gql'), 53 | 'variables' => ['test2NodeId' => $test2Node->id()], 54 | ]) 55 | ); 56 | 57 | $this->assertEquals(drupal_static('graphql_views_test:view:args'), [ 58 | 'graphql_test:contextual_title_arg' => [ 59 | 0 => [NULL], 60 | 1 => ['X'], 61 | ], 62 | 'graphql_test:contextual_node' => [ 63 | 0 => [NULL], 64 | 1 => ['X'], 65 | 2 => ['1'], 66 | 3 => ['X'], 67 | 4 => ['1'], 68 | 5 => ['X'], 69 | ], 70 | 'graphql_test:contextual_nodetest' => [ 71 | 0 => [NULL], 72 | 1 => ['X'], 73 | 2 => ['1'], 74 | 3 => ['X'], 75 | ], 76 | 'graphql_test:contextual_node_and_nodetest' => [ 77 | 0 => [NULL, NULL], 78 | 1 => ['X', 'X'], 79 | 2 => [$test2Node->id(), NULL], 80 | 3 => ['X', 'X'], 81 | 4 => ['1', '1'], 82 | 5 => ['X', 'X'], 83 | ], 84 | ]); 85 | } 86 | 87 | /** 88 | * Test if view fields are attached to correct types. 89 | */ 90 | public function testContextualViewFields() { 91 | $schema = $this->introspect(); 92 | 93 | $field = 'graphqlTestContextualTitleArgView'; 94 | $this->assertArrayHasKey($field, $schema['types']['Query']['fields']); 95 | $this->assertArrayNotHasKey($field, $schema['types']['Node']['fields']); 96 | $this->assertArrayNotHasKey($field, $schema['types']['NodeTest']['fields']); 97 | 98 | $field = 'graphqlTestContextualNodeView'; 99 | $this->assertArrayHasKey($field, $schema['types']['Query']['fields']); 100 | $this->assertArrayHasKey($field, $schema['types']['Node']['fields']); 101 | $this->assertArrayHasKey($field, $schema['types']['NodeTest']['fields']); 102 | 103 | $field = 'graphqlTestContextualNodetestView'; 104 | $this->assertArrayHasKey($field, $schema['types']['Query']['fields']); 105 | $this->assertArrayNotHasKey($field, $schema['types']['Node']['fields']); 106 | $this->assertArrayHasKey($field, $schema['types']['NodeTest']['fields']); 107 | 108 | $field = 'graphqlTestContextualNodeAndNodetestView'; 109 | $this->assertArrayHasKey($field, $schema['types']['Query']['fields']); 110 | $this->assertArrayHasKey($field, $schema['types']['Node']['fields']); 111 | $this->assertArrayHasKey($field, $schema['types']['NodeTest']['fields']); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /tests/src/Kernel/ViewsTest.php: -------------------------------------------------------------------------------- 1 | getQueryFromFile('simple.gql'); 28 | $this->assertResults($query, [], [ 29 | 'graphqlTestSimpleView' => [ 30 | 'results' => [ 31 | [ 32 | 'entityLabel' => 'Node A', 33 | ], [ 34 | 'entityLabel' => 'Node B', 35 | ], [ 36 | 'entityLabel' => 'Node C', 37 | ], 38 | ], 39 | ], 40 | ], $this->defaultCacheMetaData()->addCacheTags([ 41 | 'config:views.view.graphql_test', 42 | 'node:1', 43 | 'node:2', 44 | 'node:3', 45 | 'node_list', 46 | ])->addCacheContexts(['user'])); 47 | } 48 | 49 | /** 50 | * Test paging support. 51 | */ 52 | public function testPagedView() { 53 | $query = $this->getQueryFromFile('paged.gql'); 54 | $this->assertResults($query, [], [ 55 | 'page_one' => [ 56 | 'count' => count($this->letters), 57 | 'results' => [ 58 | ['entityLabel' => 'Node A'], 59 | ['entityLabel' => 'Node B'], 60 | ], 61 | ], 62 | 'page_two' => [ 63 | 'count' => count($this->letters), 64 | 'results' => [ 65 | ['entityLabel' => 'Node C'], 66 | ['entityLabel' => 'Node A'], 67 | ], 68 | ], 69 | 'page_three' => [ 70 | 'count' => count($this->letters), 71 | 'results' => [ 72 | ['entityLabel' => 'Node A'], 73 | ['entityLabel' => 'Node B'], 74 | ['entityLabel' => 'Node C'], 75 | ], 76 | ], 77 | 'page_four' => [ 78 | 'count' => count($this->letters), 79 | 'results' => [ 80 | ['entityLabel' => 'Node C'], 81 | ], 82 | ], 83 | ], $this->defaultCacheMetaData()->addCacheTags([ 84 | 'config:views.view.graphql_test', 85 | 'node:1', 86 | 'node:2', 87 | 'node:3', 88 | 'node:4', 89 | 'node:7', 90 | 'node:8', 91 | 'node:9', 92 | 'node_list', 93 | ])->addCacheContexts(['user'])); 94 | } 95 | 96 | /** 97 | * Test sorting behavior. 98 | */ 99 | public function testSortedView() { 100 | $query = $this->getQueryFromFile('sorted.gql'); 101 | $this->assertResults($query, [], [ 102 | 'default' => [ 103 | 'results' => [ 104 | ['entityLabel' => 'Node A'], 105 | ['entityLabel' => 'Node B'], 106 | ['entityLabel' => 'Node C'], 107 | ], 108 | ], 109 | 'asc' => [ 110 | 'results' => [ 111 | ['entityLabel' => 'Node A'], 112 | ['entityLabel' => 'Node A'], 113 | ['entityLabel' => 'Node A'], 114 | ], 115 | ], 116 | 'desc' => [ 117 | 'results' => [ 118 | ['entityLabel' => 'Node C'], 119 | ['entityLabel' => 'Node C'], 120 | ['entityLabel' => 'Node C'], 121 | ], 122 | ], 123 | 'asc_nid' => [ 124 | 'results' => [ 125 | ['entityLabel' => 'Node A'], 126 | ['entityLabel' => 'Node B'], 127 | ['entityLabel' => 'Node C'], 128 | ], 129 | ], 130 | 'desc_nid' => [ 131 | 'results' => [ 132 | ['entityLabel' => 'Node C'], 133 | ['entityLabel' => 'Node B'], 134 | ['entityLabel' => 'Node A'], 135 | ], 136 | ], 137 | ], $this->defaultCacheMetaData()->addCacheTags([ 138 | 'config:views.view.graphql_test', 139 | 'node:1', 140 | 'node:2', 141 | 'node:3', 142 | 'node:4', 143 | 'node:6', 144 | 'node:7', 145 | 'node:8', 146 | 'node:9', 147 | 'node_list', 148 | ])->addCacheContexts(['user'])); 149 | } 150 | 151 | /** 152 | * Test filter behavior. 153 | */ 154 | public function testFilteredView() { 155 | $query = <<assertResults($query, [], [ 167 | 'default' => [ 168 | 'results' => [ 169 | ['entityLabel' => 'Node A'], 170 | ['entityLabel' => 'Node A'], 171 | ['entityLabel' => 'Node A'], 172 | ], 173 | ], 174 | ], $this->defaultCacheMetaData()->addCacheTags([ 175 | 'config:views.view.graphql_test', 176 | 'node:1', 177 | 'node:4', 178 | 'node:7', 179 | 'node_list', 180 | ])->addCacheContexts(['user'])); 181 | } 182 | 183 | /** 184 | * Test filter behavior. 185 | */ 186 | public function testMultiValueFilteredView() { 187 | $query = <<assertResults($query, [], [ 197 | 'multi' => [ 198 | 'results' => [ 199 | ['entityLabel' => 'Node A'], 200 | ['entityLabel' => 'Node B'], 201 | ['entityLabel' => 'Node A'], 202 | ['entityLabel' => 'Node B'], 203 | ['entityLabel' => 'Node A'], 204 | ['entityLabel' => 'Node B'], 205 | ], 206 | ], 207 | ], $this->defaultCacheMetaData()->addCacheTags([ 208 | 'config:views.view.graphql_test', 209 | 'node:1', 210 | 'node:2', 211 | 'node:4', 212 | 'node:5', 213 | 'node:7', 214 | 'node:8', 215 | 'node_list', 216 | 217 | ])->addCacheContexts(['user'])); 218 | } 219 | 220 | /** 221 | * Test complex filters. 222 | */ 223 | public function testComplexFilteredView() { 224 | $query = <<assertResults($query, [], [ 234 | 'complex' => [ 235 | 'results' => [ 236 | ['entityLabel' => 'Node A'], 237 | ['entityLabel' => 'Node B'], 238 | ['entityLabel' => 'Node C'], 239 | ['entityLabel' => 'Node A'], 240 | ['entityLabel' => 'Node B'], 241 | ['entityLabel' => 'Node C'], 242 | ['entityLabel' => 'Node A'], 243 | ['entityLabel' => 'Node B'], 244 | ['entityLabel' => 'Node C'], 245 | ], 246 | ], 247 | ], $this->defaultCacheMetaData()->addCacheTags([ 248 | 'config:views.view.graphql_test', 249 | 'node:1', 250 | 'node:2', 251 | 'node:3', 252 | 'node:4', 253 | 'node:5', 254 | 'node:6', 255 | 'node:7', 256 | 'node:8', 257 | 'node:9', 258 | 'node_list', 259 | ])->addCacheContexts(['user'])); 260 | } 261 | 262 | /** 263 | * Test the result type for views with a single-value bundle filter. 264 | */ 265 | public function testSingleValueBundleFilterView() { 266 | $query = $this->getQueryFromFile('single_bundle_filter.gql'); 267 | $this->assertResults($query, [], [ 268 | 'withSingleBundleFilter' => [ 269 | 'results' => [ 270 | 0 => [ 271 | '__typename' => 'NodeTest', 272 | ], 273 | ], 274 | ], 275 | ], $this->defaultCacheMetaData()->addCacheTags([ 276 | 'config:views.view.graphql_bundle_test', 277 | 'node:1', 278 | 'node_list', 279 | ])); 280 | } 281 | 282 | } 283 | -------------------------------------------------------------------------------- /tests/src/Kernel/ViewsTestBase.php: -------------------------------------------------------------------------------- 1 | installEntitySchema('view'); 50 | $this->installEntitySchema('taxonomy_term'); 51 | $this->installConfig(['node', 'filter', 'views', 'graphql_views_test']); 52 | $this->createEntityReferenceField('node', 'test', 'field_tags', 'Tags', 'taxonomy_term'); 53 | 54 | Vocabulary::create([ 55 | 'name' => 'Tags', 56 | 'vid' => 'tags', 57 | ])->save(); 58 | 59 | $terms = []; 60 | 61 | $terms['A'] = Term::create([ 62 | 'name' => 'Term A', 63 | 'vid' => 'tags', 64 | ]); 65 | $terms['A']->save(); 66 | 67 | $terms['B'] = Term::create([ 68 | 'name' => 'Term B', 69 | 'vid' => 'tags', 70 | ]); 71 | $terms['B']->save(); 72 | 73 | $terms['C'] = Term::create([ 74 | 'name' => 'Term C', 75 | 'vid' => 'tags', 76 | ]); 77 | $terms['C']->save(); 78 | 79 | foreach ($this->letters as $index => $letter) { 80 | $this->createNode([ 81 | 'title' => 'Node ' . $letter, 82 | 'type' => 'test', 83 | 'field_tags' => $terms[$letter], 84 | ])->save(); 85 | } 86 | } 87 | 88 | } 89 | --------------------------------------------------------------------------------