diff --git a/src/GraphQL.php b/src/GraphQL.php index ad6d8fa5dd..d3e9bb9bde 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -65,6 +65,16 @@ public function execute($query, $context = null, $variables = [], $rootValue = n $result = $this->queryAndReturnResult($query, $context, $variables, $rootValue); if (! empty($result->errors)) { + foreach ($result->errors as $error) { + if ($error instanceof \Exception) { + info('GraphQL Error:', [ + 'code' => $error->getCode(), + 'message' => $error->getMessage(), + 'trace' => $error->getTraceAsString(), + ]); + } + } + return [ 'data' => $result->data, 'errors' => array_map([$this, 'formatError'], $result->errors), @@ -112,6 +122,26 @@ public function buildSchema() return $this->schema()->build($schema); } + /** + * Batch field resolver. + * + * @param string $abstract + * @param mixed $key + * @param array $data + * @param string $name + * + * @return \GraphQL\Deferred + */ + public function batch($abstract, $key, array $data = [], $name = null) + { + $name = $name ?: $abstract; + $instance = app()->has($name) + ? resolve($name) + : app()->instance($name, resolve($abstract)); + + return $instance->load($key, $data); + } + /** * Get an instance of the schema builder. * diff --git a/src/Schema/Directives/Fields/BelongsTo.php b/src/Schema/Directives/Fields/BelongsTo.php index 818d74e03e..dff44e9884 100644 --- a/src/Schema/Directives/Fields/BelongsTo.php +++ b/src/Schema/Directives/Fields/BelongsTo.php @@ -4,6 +4,7 @@ use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; +use Nuwave\Lighthouse\Support\DataLoader\Loaders\BelongsToLoader; use Nuwave\Lighthouse\Support\Traits\HandlesDirectives; class BelongsTo implements FieldResolver @@ -36,7 +37,11 @@ public function handle(FieldValue $value) ); return $value->setResolver(function ($root, array $args) use ($relation) { - return $root->{$relation}; + return graphql()->batch(BelongsToLoader::class, $root->getKey(), [ + 'relation' => $relation, + 'root' => $root, + 'args' => $args, + ]); }); } } diff --git a/src/Schema/Directives/Fields/HasManyDirective.php b/src/Schema/Directives/Fields/HasManyDirective.php index 34bf190a72..46aa2a6eea 100644 --- a/src/Schema/Directives/Fields/HasManyDirective.php +++ b/src/Schema/Directives/Fields/HasManyDirective.php @@ -8,6 +8,7 @@ use Nuwave\Lighthouse\Schema\Types\PaginatorField; use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; +use Nuwave\Lighthouse\Support\DataLoader\Loaders\HasManyLoader; use Nuwave\Lighthouse\Support\Exceptions\DirectiveException; use Nuwave\Lighthouse\Support\Traits\CanParseTypes; use Nuwave\Lighthouse\Support\Traits\HandlesDirectives; @@ -141,13 +142,10 @@ protected function connectionTypeResolver($relation, FieldValue $value) }); return function ($parent, array $args, $context = null, ResolveInfo $info = null) use ($relation, $scopes) { - $builder = call_user_func([$parent, $relation]); - - return $builder->when(! empty($scopes), function ($q) use ($scopes, $args) { - foreach ($scopes as $scope) { - call_user_func_array([$q, $scope], [$args]); - } - })->relayConnection($args); + return graphql()->batch(HasManyLoader::class, $parent->getKey(), array_merge( + compact('relation', 'parent', 'args', 'scopes'), + ['type' => 'relay'] + ), camel_case($parent->getTable().'_'.$relation)); }; } @@ -195,13 +193,10 @@ protected function paginatorTypeResolver($relation, FieldValue $value) }); return function ($parent, array $args, $context = null, ResolveInfo $info = null) use ($relation, $scopes) { - $builder = call_user_func([$parent, $relation]); - - return $builder->when(! empty($scopes), function ($q) use ($scopes, $args) { - foreach ($scopes as $scope) { - call_user_func_array([$q, $scope], [$args]); - } - })->paginatorConnection($args); + return graphql()->batch(HasManyLoader::class, $parent->getKey(), array_merge( + compact('relation', 'parent', 'args', 'scopes'), + ['type' => 'paginator'] + ), camel_case($parent->getTable().'_'.$relation)); }; } @@ -218,14 +213,10 @@ protected function defaultResolver($relation, FieldValue $value) $scopes = $this->getScopes($value); return function ($parent, array $args) use ($relation, $scopes) { - // TODO: Wrap w/ data loader to prevent N+1 - $builder = call_user_func([$parent, $relation]); - // TODO: Create scopeGqlQuery scope to allow adjustments for $args. - return $builder->when(! empty($scopes), function ($q) use ($scopes, $args) { - foreach ($scopes as $scope) { - call_user_func_array([$q, $scope], [$args]); - } - })->get(); + return graphql()->batch(HasManyLoader::class, $parent->getKey(), array_merge( + compact('relation', 'parent', 'args', 'scopes'), + ['type' => 'default'] + ), camel_case($parent->getTable().'_'.$relation)); }; } diff --git a/src/Support/DataLoader/BatchLoader.php b/src/Support/DataLoader/BatchLoader.php new file mode 100644 index 0000000000..9b362c84e0 --- /dev/null +++ b/src/Support/DataLoader/BatchLoader.php @@ -0,0 +1,62 @@ +keys[$key] = $data; + + return new Deferred(function () use ($key) { + if (! $this->hasLoaded) { + $this->resolve(); + $this->hasLoaded = true; + } + + return array_get($this->keys, "$key.value"); + }); + } + + /** + * Set key value. + * + * @param mixed $key + * @param mixed $value + */ + protected function set($key, $value) + { + if ($field = array_get($this->keys, $key)) { + $this->keys[$key] = array_merge($field, compact('value')); + } + } + + /** + * Resolve keys. + */ + abstract public function resolve(); +} diff --git a/src/Support/DataLoader/Loaders/BelongsToLoader.php b/src/Support/DataLoader/Loaders/BelongsToLoader.php new file mode 100644 index 0000000000..b1e3825597 --- /dev/null +++ b/src/Support/DataLoader/Loaders/BelongsToLoader.php @@ -0,0 +1,26 @@ +keys)->map(function ($item) { + return array_merge($item, ['json' => json_encode($item['args'])]); + })->groupBy('json')->each(function ($items) { + $relation = array_get($items->first(), 'relation'); + $models = $items->pluck('root'); + + $models->fetch([$relation]); + $models->each(function ($model) use ($relation) { + $this->set($model->id, $model->getRelation($relation)); + }); + }); + } +} diff --git a/src/Support/DataLoader/Loaders/HasManyLoader.php b/src/Support/DataLoader/Loaders/HasManyLoader.php new file mode 100644 index 0000000000..cad5c6d3c8 --- /dev/null +++ b/src/Support/DataLoader/Loaders/HasManyLoader.php @@ -0,0 +1,55 @@ +keys)->map(function ($item) { + return array_merge($item, ['json' => json_encode($item['args'])]); + })->groupBy('json')->each(function ($items) { + $first = $items->first(); + $parents = $items->pluck('parent'); + $scopes = array_get($first, 'scopes', []); + $relation = $first['relation']; + $type = $first['type']; + $args = $first['args']; + + $constraints = [$relation => function ($q) use ($scopes, $args) { + foreach ($scopes as $scope) { + call_user_func_array([$q, $scope], [$args]); + } + }]; + + switch ($type) { + case 'relay': + $first = data_get($args, 'first', 15); + $after = $this->decodeCursor($args); + $currentPage = $first && $after ? floor(($first + $after) / $first) : 1; + $parents->fetchForPage($first, $currentPage, $constraints); + break; + case 'paginator': + $first = data_get($args, 'count', 15); + $page = data_get($args, 'page', 1); + $parents->fetchForPage($first, $page, $constraints); + break; + default: + $parents->fetch($constraints); + break; + } + + $parents->each(function ($model) use ($relation) { + $this->set($model->id, $model->getRelation($relation)); + }); + }); + } +}