Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions config/graphql.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,33 @@
'users' => false,
],

/*
|--------------------------------------------------------------------------
| Improved Types
|--------------------------------------------------------------------------
|
| When enabled, fields like entries and terms can return dynamically
| generated union types when multiple blueprints are possible. Also will
| use non-nullable types for entries and terms.
|
| You may also register per-collection and per-taxonomy queries that
| return typed results. List collection or taxonomy handles under
| "collections" and "terms", or use "*" to enable all.
|
*/

'improved_types' => [
'enabled' => env('STATAMIC_GRAPHQL_IMPROVED_TYPES', true),
'collections' => [
// 'blog_posts',
// '*',
],
'terms' => [
// 'tags',
// '*',
],
],

/*
|--------------------------------------------------------------------------
| Authentication
Expand Down
17 changes: 15 additions & 2 deletions src/Fieldtypes/Assets/Assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Statamic\Fields\Fieldtype;
use Statamic\Fieldtypes\UpdatesReferences;
use Statamic\GraphQL\Types\AssetInterface;
use Statamic\GraphQL\Types\AssetType;
use Statamic\Http\Resources\CP\Assets\AssetsFieldtypeAsset as AssetResource;
use Statamic\Query\Scopes\Filter;
use Statamic\Support\Arr;
Expand Down Expand Up @@ -492,10 +493,22 @@ protected function getItemsForPreProcessIndex($values): Collection

public function toGqlType()
{
$type = GraphQL::type(AssetInterface::NAME);
// Fallback to old behaviour if improved types are disabled.
if (! config('statamic.graphql.improved_types.enabled', false)) {
$type = GraphQL::type(AssetInterface::NAME);

if ($this->config('max_files') !== 1) {
$type = GraphQL::listOf($type);
}

return $type;
}

$container = $this->container();
$type = GraphQL::type(AssetType::buildName($container));

if ($this->config('max_files') !== 1) {
$type = GraphQL::listOf($type);
$type = GraphQL::listOf(GraphQL::nonNull($type));
}

return $type;
Expand Down
28 changes: 26 additions & 2 deletions src/Fieldtypes/Entries.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use Statamic\Facades\Search;
use Statamic\Facades\Site;
use Statamic\Facades\User;
use Statamic\GraphQL\Types\DynamicEntryUnionType;
use Statamic\GraphQL\Types\EntryInterface;
use Statamic\Http\Resources\CP\Entries\EntriesFieldtypeEntries;
use Statamic\Http\Resources\CP\Entries\EntriesFieldtypeEntry as EntryResource;
use Statamic\Query\OrderBy;
Expand Down Expand Up @@ -455,10 +457,32 @@ protected function getConfiguredCollections()

public function toGqlType()
{
$type = GraphQL::type('EntryInterface');
// Fallback to old behaviour if improved types are disabled.
if (! config('statamic.graphql.improved_types.enabled', false)) {
$type = GraphQL::type('EntryInterface');

if ($this->config('max_items') !== 1) {
$type = GraphQL::listOf($type);
}

return $type;
}

// If the fieldtype isn't constrained to specific collections, return the generic EntryInterface.
if (empty($this->config('collections'))) {
$type = GraphQL::type(EntryInterface::NAME);

if ($this->config('max_items') !== 1) {
$type = GraphQL::listOf(GraphQL::nonNull($type));
}

return $type;
}

$type = DynamicEntryUnionType::createTypeFor($this->getConfiguredCollections());

if ($this->config('max_items') !== 1) {
$type = GraphQL::listOf($type);
$type = GraphQL::listOf(GraphQL::nonNull($type));
}

return $type;
Expand Down
26 changes: 24 additions & 2 deletions src/Fieldtypes/Terms.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Statamic\Facades\Taxonomy;
use Statamic\Facades\Term;
use Statamic\Facades\User;
use Statamic\GraphQL\Types\DynamicTermUnionType;
use Statamic\GraphQL\Types\TermInterface;
use Statamic\Http\Resources\CP\Taxonomies\TermsFieldtypeTerms as TermsResource;
use Statamic\Query\OrderBy;
Expand Down Expand Up @@ -529,10 +530,31 @@ protected function getConfiguredTaxonomies()

public function toGqlType()
{
$type = GraphQL::type(TermInterface::NAME);
if (! config('statamic.graphql.improved_types.enabled', false)) {
$type = GraphQL::type(TermInterface::NAME);

if ($this->config('max_items') !== 1) {
$type = GraphQL::listOf($type);
}

return $type;
}

// If the fieldtype isn't constrained to specific taxonomies, return the generic TermInterface.
if (empty($this->field()->config()['taxonomies'])) {
$type = GraphQL::type(TermInterface::NAME);

if ($this->config('max_items') !== 1) {
$type = GraphQL::listOf(GraphQL::nonNull($type));
}

return $type;
}

$type = DynamicTermUnionType::createTypeFor($this->getConfiguredTaxonomies());

if ($this->config('max_items') !== 1) {
$type = GraphQL::listOf($type);
$type = GraphQL::listOf(GraphQL::nonNull($type));
}

return $type;
Expand Down
80 changes: 80 additions & 0 deletions src/GraphQL/DefaultSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

use Facades\Statamic\API\ResourceAuthorizer;
use Rebing\GraphQL\Support\Contracts\ConfigConvertible;
use Statamic\Facades\Collection;
use Statamic\Facades\GraphQL;
use Statamic\Facades\Taxonomy;
use Statamic\GraphQL\Middleware\CacheResponse;
use Statamic\GraphQL\Middleware\HandleAuthentication;
use Statamic\GraphQL\Queries\AssetContainerQuery;
Expand All @@ -23,6 +25,10 @@
use Statamic\GraphQL\Queries\NavsQuery;
use Statamic\GraphQL\Queries\PingQuery;
use Statamic\GraphQL\Queries\SitesQuery;
use Statamic\GraphQL\Queries\SpecificEntriesQuery;
use Statamic\GraphQL\Queries\SpecificEntryQuery;
use Statamic\GraphQL\Queries\SpecificTermQuery;
use Statamic\GraphQL\Queries\SpecificTermsQuery;
use Statamic\GraphQL\Queries\TaxonomiesQuery;
use Statamic\GraphQL\Queries\TaxonomyQuery;
use Statamic\GraphQL\Queries\TermQuery;
Expand Down Expand Up @@ -69,12 +75,86 @@ private function getQueries()
$queries = $queries->merge(ResourceAuthorizer::isAllowed('graphql', $resource) ? $qs : []);
});

$queries = $queries
->merge($this->getSpecificEntriesQueries())
->merge($this->getSpecificTermsQueries());

return $queries
->merge(config('statamic.graphql.queries', []))
->merge(GraphQL::getExtraQueries())
->all();
}

private function getSpecificEntriesQueries(): array
{
// rebing/graphql-laravel calls toConfig() eagerly during boot
// at which point the Stache is not yet ready.
// The schema is rebuilt when an actual request hits the controller,
// where the Stache is fully booted, so wildcards still expand correctly there.
if (! app()->isBooted()) {
return [];
}

if (! ResourceAuthorizer::isAllowed('graphql', 'collections')) {
return [];
}

$configured = config('statamic.graphql.improved_types.collections', []);

if (empty($configured)) {
return [];
}

if (in_array('*', $configured)) {
$handles = Collection::handles()->all();
} else {
$handles = $configured;
}

$allowed = ResourceAuthorizer::allowedSubResources('graphql', 'collections');

return collect($handles)
->filter(fn ($handle) => in_array($handle, $allowed))
->flatMap(fn ($handle) => [
new SpecificEntriesQuery($handle),
new SpecificEntryQuery($handle),
])
->all();
}

private function getSpecificTermsQueries(): array
{
if (! app()->isBooted()) {
return [];
}

if (! ResourceAuthorizer::isAllowed('graphql', 'taxonomies')) {
return [];
}

$configured = config('statamic.graphql.improved_types.terms', []);

if (empty($configured)) {
return [];
}

if (in_array('*', $configured)) {
$handles = Taxonomy::handles()->all();
} else {
$handles = $configured;
}

$allowed = ResourceAuthorizer::allowedSubResources('graphql', 'taxonomies');

return collect($handles)
->filter(fn ($handle) => in_array($handle, $allowed))
->flatMap(fn ($handle) => [
new SpecificTermsQuery($handle),
new SpecificTermQuery($handle),
])
->all();
}

private function getMiddleware()
{
return array_merge(
Expand Down
117 changes: 117 additions & 0 deletions src/GraphQL/Queries/SpecificEntriesQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

namespace Statamic\GraphQL\Queries;

use Facades\Statamic\API\FilterAuthorizer;
use Facades\Statamic\API\QueryScopeAuthorizer;
use GraphQL\Type\Definition\Type;
use Statamic\Facades\Collection;
use Statamic\Facades\Entry;
use Statamic\Facades\GraphQL;
use Statamic\GraphQL\Middleware\AuthorizeFilters;
use Statamic\GraphQL\Middleware\AuthorizeQueryScopes;
use Statamic\GraphQL\Middleware\ResolvePage;
use Statamic\GraphQL\Queries\Concerns\FiltersQuery;
use Statamic\GraphQL\Queries\Concerns\ScopesQuery;
use Statamic\GraphQL\Types\DynamicEntryUnionType;
use Statamic\GraphQL\Types\JsonArgument;
use Statamic\Query\OrderBy;
use Statamic\Support\Str;

class SpecificEntriesQuery extends Query
{
use FiltersQuery {
filterQuery as traitFilterQuery;
}

use ScopesQuery;

protected $middleware = [
ResolvePage::class,
AuthorizeFilters::class,
AuthorizeQueryScopes::class,
];

public function __construct(protected string $collectionHandle)
{
$this->attributes['name'] = Str::camel($collectionHandle);

parent::__construct();
}

public function type(): Type
{
$collection = Collection::findByHandle($this->collectionHandle);

return GraphQL::nonNull(GraphQL::paginate(DynamicEntryUnionType::createTypeFor($collection)));
}

public function args(): array
{
return [
'limit' => GraphQL::int(),
'page' => GraphQL::int(),
'filter' => GraphQL::type(JsonArgument::NAME),
'query_scope' => GraphQL::type(JsonArgument::NAME),
'sort' => GraphQL::listOf(GraphQL::string()),
'site' => GraphQL::string(),
];
}

public function resolve($root, $args)
{
$query = Entry::query();

$query->where('collection', $this->collectionHandle);

if ($site = $args['site'] ?? null) {
$query->where('site', $site);
}

$this->filterQuery($query, $args['filter'] ?? []);

$this->scopeQuery($query, $args['query_scope'] ?? []);

$this->sortQuery($query, $args['sort'] ?? []);

return $query->paginate($args['limit'] ?? 1000);
}

private function filterQuery($query, $filters)
{
if (! isset($filters['status']) && ! isset($filters['published'])) {
$filters['status'] = 'published';
}

$this->traitFilterQuery($query, $filters);
}

private function sortQuery($query, $sorts)
{
if (empty($sorts)) {
$sorts = ['id'];
}

foreach ($sorts as $sort) {
$order = 'asc';

if (Str::contains($sort, ' ')) {
[$sort, $order] = explode(' ', $sort);
}

if ($sort = OrderBy::column($sort)) {
$query->orderBy($sort, $order);
}
}
}

public function allowedFilters($args)
{
return FilterAuthorizer::allowedForSubResources('graphql', 'collections', $this->collectionHandle);
}

public function allowedScopes($args)
{
return QueryScopeAuthorizer::allowedForSubResources('graphql', 'collections', $this->collectionHandle);
}
}
Loading
Loading