From fc5b97641c3dd54a6fd742e77aac12aee88d9e0b Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Thu, 28 Jan 2016 14:22:44 -0700 Subject: [PATCH 01/66] collecting collections on query --- src/Schema/Connection.php | 77 ++++++++++++++++ src/Schema/Parser.php | 131 ++++++++++++++++++++++++++++ src/Schema/SchemaContainer.php | 155 ++++++++++++++++++++++++++------- src/Traits/RelayMiddleware.php | 38 ++------ 4 files changed, 339 insertions(+), 62 deletions(-) create mode 100644 src/Schema/Connection.php create mode 100644 src/Schema/Parser.php diff --git a/src/Schema/Connection.php b/src/Schema/Connection.php new file mode 100644 index 0000000..98dea09 --- /dev/null +++ b/src/Schema/Connection.php @@ -0,0 +1,77 @@ +arguments); + } + + /** + * Set arguments of selection. + * + * @param Field $field + */ + public function setArguments(Field $field) + { + if ($field->arguments) { + foreach ($field->arguments as $argument) { + $this->arguments[$argument->name->value] = $argument->value->value; + } + } + } + + /** + * Set connection path. + * + * @param string $path + */ + public function setPath($path = '') + { + $this->path = $path; + } +} diff --git a/src/Schema/Parser.php b/src/Schema/Parser.php new file mode 100644 index 0000000..0a62d34 --- /dev/null +++ b/src/Schema/Parser.php @@ -0,0 +1,131 @@ +initialize(); + + $this->parseFields($selectionSet, $root); + + return $this->connections; + } + + /** + * Set the selection set. + * + * @return void + */ + public function initialize() + { + $this->depth = 0; + $this->path = []; + $this->connections = []; + } + + /** + * Determine if field has selection set. + * + * @param Field $field + * @return boolean + */ + protected function hasChildren($field) + { + return $this->isField($field) && isset($field->selectionSet) && !empty($field->selectionSet->selections); + } + + /** + * Determine if name is a relay edge. + * + * @param string $name + * @return boolean + */ + protected function isEdge($name) + { + return in_array($name, $this->relayEdges); + } + + /** + * Parse arguments. + * + * @param array $selectionSet + * @param string $root + * @return void + */ + protected function parseFields(array $selectionSet = [], $root = '') + { + foreach ($selectionSet as $field) { + if ($this->hasChildren($field)) { + $name = $field->name->value;; + + if (!$this->isEdge($name)) { + $this->path[] = $name; + + $connection = new Connection; + $connection->name = $name; + $connection->root = $root; + $connection->path = implode('.', $this->path); + $connection->depth = count($this->path); + $connection->setArguments($field); + + $this->connections[] = $connection; + } + + $this->parseFields($field->selectionSet->selections, $root); + } + } + + array_pop($this->path); + } +} diff --git a/src/Schema/SchemaContainer.php b/src/Schema/SchemaContainer.php index 8a5a5c1..17c859f 100644 --- a/src/Schema/SchemaContainer.php +++ b/src/Schema/SchemaContainer.php @@ -3,8 +3,11 @@ namespace Nuwave\Relay\Schema; use Closure; +use Illuminate\Http\Request; use Nuwave\Relay\Schema\FieldCollection as Collection; use Nuwave\Relay\Schema\Field; +use GraphQL\Language\Source; +use GraphQL\Language\Parser as GraphQLParser; class SchemaContainer { @@ -43,18 +46,61 @@ class SchemaContainer */ protected $namespace = ''; + /** + * Schema parser. + * + * @var Parser + */ + public $parser; + + /** + * Middleware to be applied to query. + * + * @var array + */ + public $middleware = []; + + /** + * Connections present in query. + * + * @var array + */ + public $connections = []; + /** * Create new instance of Mutation container. * * @return void */ - public function __construct() + public function __construct(Parser $parser) { + $this->parser = $parser; + $this->mutations = new Collection; $this->queries = new Collection; $this->types = new Collection; } + /** + * Set up the graphql request. + * + * @param $query string + * @return void + */ + public function setupRequest($query = 'GraphGL request', $operation = 'query') + { + $source = new Source($query); + $ast = GraphQLParser::parse($source); + + if (isset($ast->definitions[0])) { + $d = $ast->definitions[0]; + $operation = $d->operation ?: 'query'; + $selectionSet = $d->selectionSet->selections; + + $this->parseSelections($selectionSet, $operation); + } + } + /** * Add mutation to collection. * @@ -103,35 +149,6 @@ public function type($name, $namespace) return $type; } - /** - * Get class name. - * - * @param string $namespace - * @return string - */ - protected function getClassName($namespace) - { - return empty(trim($this->namespace)) ? $namespace : trim($this->namespace, '\\') . '\\' . $namespace; - } - - /** - * Get field and attach necessary middleware. - * - * @param string $name - * @param string $namespace - * @return Field - */ - protected function createField($name, $namespace) - { - $field = new Field($name, $this->getClassName($namespace)); - - if ($this->hasMiddlewareStack()) { - $field->addMiddleware($this->middlewareStack); - } - - return $field; - } - /** * Group child elements. * @@ -258,6 +275,84 @@ public function findType($name) return $this->getTypes()->pull($name); } + /** + * Get the middlware for the query. + * + * @return array + */ + public function middleware() + { + return $this->middleware; + } + + /** + * Get connections for the query. + * + * @return array + */ + public function connections() + { + return $this->connections; + } + + /** + * Initialize schema. + * + * @param array $selectionSet + * @return void + */ + protected function parseSelections(array $selectionSet = [], $operation = '') + { + foreach ($selectionSet as $selection) { + if ($this->parser->isField($selection)) { + $schema = $this->find($selection->name->value, $operation); + + if (isset($schema['middleware']) && !empty($schema['middleware'])) { + $this->middleware = array_merge($this->middleware, $schema['middleware']); + } + + if (isset($selection->selectionSet) && !empty($selection->selectionSet->selections)) { + $this->connections = array_merge( + $this->connections, + $this->parser->getConnections( + $selection->selectionSet->selections, + $selection->name->value + ) + ); + } + } + } + } + + /** + * Get class name. + * + * @param string $namespace + * @return string + */ + protected function getClassName($namespace) + { + return empty(trim($this->namespace)) ? $namespace : trim($this->namespace, '\\') . '\\' . $namespace; + } + + /** + * Get field and attach necessary middleware. + * + * @param string $name + * @param string $namespace + * @return Field + */ + protected function createField($name, $namespace) + { + $field = new Field($name, $this->getClassName($namespace)); + + if ($this->hasMiddlewareStack()) { + $field->addMiddleware($this->middlewareStack); + } + + return $field; + } + /** * Check if middleware stack is empty. * diff --git a/src/Traits/RelayMiddleware.php b/src/Traits/RelayMiddleware.php index c911a22..601aba3 100644 --- a/src/Traits/RelayMiddleware.php +++ b/src/Traits/RelayMiddleware.php @@ -9,44 +9,18 @@ trait RelayMiddleware { /** - * Middleware to be attached to GraphQL query. - * - * @var array - */ - protected $relayMiddleware = []; - - /** - * Genarate middleware to be run on query. + * Genarate middleware and connections from query. * * @param Request $request * @return array */ - public function queryMiddleware(Request $request) + public function setupQuery(Request $request) { - $relay = app('relay'); - $source = new Source($request->get('query', 'GraphQL request')); - $ast = Parser::parse($source); + $relay = app('relay'); + $relay->setupRequest($request->get('query')); - if (isset($ast->definitions[0])) { - $d = $ast->definitions[0]; - $operation = $d->operation ?: 'query'; - $selectionSet = $d->selectionSet->selections; - - foreach ($selectionSet as $selection) { - if (is_object($selection) && $selection instanceof \GraphQL\Language\AST\Field) { - try { - $schema = $relay->find($selection->name->value, $operation); - - if (isset($schema['middleware']) && !empty($schema['middleware'])) { - $this->relayMiddleware = array_merge($this->relayMiddleware, $schema['middleware']); - } - } catch (\Exception $e) { - continue; - } - } - } + foreach ($relay->middleware() as $middleware) { + $this->middleware($middleware); } - - return array_unique($this->relayMiddleware); } } From 753a4fa4a29a2321c86e11c2d7b4cc0261a69f8b Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Thu, 28 Jan 2016 14:28:40 -0700 Subject: [PATCH 02/66] inject parser --- src/ServiceProvider.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 24e6278..ce5df13 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -2,6 +2,7 @@ namespace Nuwave\Relay; +use Nuwave\Relay\Schema\Parser; use Nuwave\Relay\Schema\SchemaContainer; use Illuminate\Support\ServiceProvider as BaseProvider; @@ -33,7 +34,7 @@ public function register() ]); $this->app->singleton('relay', function ($app) { - return new SchemaContainer; + return new SchemaContainer(new Parser); }); $this->app->alias('relay', SchemaContainer::class); From fd55d6fdd314a861d758e2313fcded40b64f0cb3 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Thu, 28 Jan 2016 14:35:46 -0700 Subject: [PATCH 03/66] rename ast field namespace --- src/Schema/Connection.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Schema/Connection.php b/src/Schema/Connection.php index 98dea09..4b5148e 100644 --- a/src/Schema/Connection.php +++ b/src/Schema/Connection.php @@ -2,7 +2,7 @@ namespace Nuwave\Relay\Schema; -use GraphQL\Language\AST\Field; +use GraphQL\Language\AST\ASTField; class Connection { @@ -54,9 +54,9 @@ public function hasArguments() /** * Set arguments of selection. * - * @param Field $field + * @param ASTField $field */ - public function setArguments(Field $field) + public function setArguments(ASTField $field) { if ($field->arguments) { foreach ($field->arguments as $argument) { From 21afcbc6c4b8960d141c3b25da39b998025c9d64 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Thu, 28 Jan 2016 14:38:58 -0700 Subject: [PATCH 04/66] fix namespace --- src/Schema/Connection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Schema/Connection.php b/src/Schema/Connection.php index 4b5148e..1763086 100644 --- a/src/Schema/Connection.php +++ b/src/Schema/Connection.php @@ -2,7 +2,7 @@ namespace Nuwave\Relay\Schema; -use GraphQL\Language\AST\ASTField; +use GraphQL\Language\AST\Field as ASTField; class Connection { From f4b96d7dfcdd794ecbb7e7d5fa25e2a928d93e4b Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Thu, 28 Jan 2016 15:09:53 -0700 Subject: [PATCH 05/66] avoid including connections in mutations (for now) --- src/Traits/MutationTestTrait.php | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Traits/MutationTestTrait.php b/src/Traits/MutationTestTrait.php index 34baf30..ed61223 100644 --- a/src/Traits/MutationTestTrait.php +++ b/src/Traits/MutationTestTrait.php @@ -84,7 +84,7 @@ protected function availableOutputFields($mutationName) $objectType = $field->getType(); if ($objectType instanceof ObjectType) { - $fields = array_keys($objectType->getFields()); + $fields = $this->includeOutputFields($objectType); $outputFields[] = $name . '{'. implode(',', $fields) .'}'; } } @@ -92,4 +92,31 @@ protected function availableOutputFields($mutationName) return $outputFields; } + + /** + * Determine if output fields should be included. + * + * @param mixed $objectType + * @return boolean + */ + protected function includeOutputFields(ObjectType $objectType) + { + $fields = []; + + foreach ($objectType->getFields() as $name => $field) { + $type = $field->getType(); + + if ($type instanceof ObjectType) { + $config = $type->config; + + if (isset($config['name']) && preg_match('/Connection$/', $config['name'])) { + continue; + } + } + + $fields[] = $name; + } + + return $fields; + } } From 91f63d34a7d9a09ca807d01318a6aac78fcd5d87 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 30 Jan 2016 09:00:54 -0700 Subject: [PATCH 06/66] added connection autoloading --- src/Schema/SchemaContainer.php | 69 ++++++++++++++++ src/Types/RelayType.php | 145 ++++++++++++++++++++++++++------- 2 files changed, 186 insertions(+), 28 deletions(-) diff --git a/src/Schema/SchemaContainer.php b/src/Schema/SchemaContainer.php index 17c859f..dd37332 100644 --- a/src/Schema/SchemaContainer.php +++ b/src/Schema/SchemaContainer.php @@ -101,6 +101,75 @@ public function setupRequest($query = 'GraphGL request', $operation = 'query') } } + /** + * Check to see if field is a parent. + * + * @param string $name + * @return boolean + */ + public function isParent($name) + { + foreach ($this->connections as $connection) { + if ($this->hasPath($connection, $name)) { + return true; + } + } + + return false; + } + + /** + * Get list of connections in query that belong + * to parent. + * + * @param string $parent + * @param array $connections + * @return array + */ + public function connectionsInRequest($parent, array $connections) + { + $queryConnections = []; + + foreach ($this->connections as $connection) { + if ($this->hasPath($connection, $parent) && isset($connections[$connection->name])) { + $queryConnections[] = $connections[$connection->name]; + } + } + + return $queryConnections; + } + + /** + * Get arguments of connection. + * + * @param string $name + * @return array + */ + public function connectionArguments($name) + { + $connection = array_first($this->connections, function ($key, $connection) use ($name) { + return $connection->name == $name; + }); + + if ($connection) { + return $connection->arguments; + } + + return []; + } + + /** + * Determine if connection has parent in it's path. + * + * @param Connection $connection + * @param string $parent + * @return boolean + */ + protected function hasPath(Connection $connection, $parent) + { + return preg_match("/{$parent}./", $connection->path); + } + /** * Add mutation to collection. * diff --git a/src/Types/RelayType.php b/src/Types/RelayType.php index 5623fe4..0980908 100644 --- a/src/Types/RelayType.php +++ b/src/Types/RelayType.php @@ -4,6 +4,7 @@ use Closure; use GraphQL; +use GraphQL\Language\AST\ListType; use Folklore\GraphQL\Support\Type as GraphQLType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\ObjectType; @@ -14,11 +15,19 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Nuwave\Relay\GlobalIdTrait; +use Nuwave\Relay\Schema\SchemaContainer; abstract class RelayType extends GraphQLType { use GlobalIdTrait; + /** + * Schema container. + * + * @var SchemaContainer + */ + protected $container; + /** * List of fields with global identifier. * @@ -87,36 +96,12 @@ public function getConnections() 'type' => Type::string() ] ], - 'resolve' => isset($edge['resolve']) ? $edge['resolve'] : function ($collection, array $args, ResolveInfo $info) use ($name) { - $items = []; - - if ($collection instanceof Model) { - $items = $collection->getAttribute($name); - } else if (is_object($collection) && method_exists($collection, 'get')) { - $items = $collection->get($name); - } else if (is_array($collection) && isset($collection[$name])) { - $items = new Collection($collection[$name]); - } + 'resolve' => function ($parent, array $args, ResolveInfo $info) use ($name, $edge) { + $collection = isset($edge['resolve']) ? $edge['resolve'] : $this->autoResolve($parent, $args, $info, $name); - if (isset($args['first'])) { - $total = $items->count(); - $first = $args['first']; - $after = $this->decodeCursor($args); - $currentPage = $first && $after ? floor(($first + $after) / $first) : 1; - - return new Paginator( - $items->slice($after)->take($first), - $total, - $first, - $currentPage - ); - } + $this->autoLoad($collection, $edge, $name); - return new Paginator( - $items, - count($items), - count($items) - ); + return $collection; } ]; } @@ -124,6 +109,76 @@ public function getConnections() return $edges; } + /** + * Auto resolve the connection. + * + * @param mixed $parent + * @param array $args + * @param ResolveInfo $info + * @param string $name + * @return Paginator + */ + protected function autoResolve($parent, array $args, ResolveInfo $info, $name = '') + { + $items = []; + + if ($parent instanceof Model) { + $items = $parent->getAttribute($name); + } else if (is_object($parent) && method_exists($parent, 'get')) { + $items = $parent->get($name); + } else if (is_array($parent) && isset($parent[$name])) { + $items = new Collection($parent[$name]); + } + + if (isset($args['first'])) { + $total = $items->count(); + $first = $args['first']; + $after = $this->decodeCursor($args); + $currentPage = $first && $after ? floor(($first + $after) / $first) : 1; + + return new Paginator( + $items->slice($after)->take($first), + $total, + $first, + $currentPage + ); + } + + return new Paginator( + $items, + count($items), + count($items) + ); + } + + /** + * Auto load the fields connection(s). + * + * @param Paginator $collection + * @param array $edge + * @param string $name + * @return void + */ + protected function autoLoad(Paginator $collection, array $edge, $name) + { + $relay = $this->getContainer(); + + if ($relay->isParent($name)) { + if ($typeClass = $this->typeFromSchema($edge['type'])) { + $type = app($typeClass); + $connections = $relay->connectionsInRequest($name, $type->connections()); + + foreach ($connections as $key => $connection) { + if (isset($connection['load'])) { + $load = $connection['load']; + + $load($collection, $relay->connectionArguments($key)); + } + } + } + } + } + /** * Generate PageInfo object type. * @@ -254,6 +309,24 @@ protected function resolveCursor($edge) return $edge->relayCursor; } + /** + * Resolve type from schema. + * + * @param ListOfType|Type $edge + * @return string|null + */ + protected function typeFromSchema($edge) + { + $type = $edge instanceof ListOfType ? $edge->getWrappedType() : $edge; + $schema = config('graphql.types'); + + if ($typeClass = array_get($schema, $type->toString())) { + return $typeClass; + } + + return array_get($schema, strtolower($type->toString())); + } + /** * Decode cursor from query arguments. * @@ -276,6 +349,22 @@ protected function getCursorId($cursor) return (int)$this->decodeRelayId($cursor); } + /** + * Get schema container instance. + * + * @return SchemaContainer + */ + protected function getContainer() + { + if ($this->container) { + return $this->container; + } + + $this->container = app('relay'); + + return $this->container; + } + /** * Available connections for type. * From 1e301e53d9ada4c16e08247cea53d055f5feffed Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 30 Jan 2016 15:15:37 -0700 Subject: [PATCH 07/66] make autoload optional --- src/Types/RelayType.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Types/RelayType.php b/src/Types/RelayType.php index 0980908..b53ddab 100644 --- a/src/Types/RelayType.php +++ b/src/Types/RelayType.php @@ -97,9 +97,11 @@ public function getConnections() ] ], 'resolve' => function ($parent, array $args, ResolveInfo $info) use ($name, $edge) { - $collection = isset($edge['resolve']) ? $edge['resolve'] : $this->autoResolve($parent, $args, $info, $name); + $collection = isset($edge['resolve']) ? $edge['resolve']($parent, $args, $info) : $this->autoResolve($parent, $args, $info, $name); - $this->autoLoad($collection, $edge, $name); + if (isset($edge['load'])) { + $this->autoLoad($collection, $edge, $name); + } return $collection; } From 622977b50e2ab04388463e538a3de3c8736a130c Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Fri, 12 Feb 2016 12:59:22 -0700 Subject: [PATCH 08/66] include reference to original relay id --- src/Mutations/MutationWithClientId.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mutations/MutationWithClientId.php b/src/Mutations/MutationWithClientId.php index c22ea7e..39a60a5 100644 --- a/src/Mutations/MutationWithClientId.php +++ b/src/Mutations/MutationWithClientId.php @@ -73,6 +73,7 @@ public function args() public function resolve($_, $args, ResolveInfo $info) { if ($this->mutatesRelayType && isset($args['input']['id'])) { + $args['input']['relay_id'] = $args['input']['id']; $args['input']['id'] = $this->decodeRelayId($args['input']['id']); } From 0e5eabae63134c531da30fec2a5a5902e0bd611c Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Fri, 12 Feb 2016 13:31:54 -0700 Subject: [PATCH 09/66] create relay file generators --- config/config.php | 34 ++++++++++---- src/Commands/FieldMakeCommand.php | 51 +++++++++++++++++++++ src/Commands/MutationMakeCommand.php | 51 +++++++++++++++++++++ src/Commands/QueryMakeCommand.php | 51 +++++++++++++++++++++ src/Commands/TypeMakeCommand.php | 51 +++++++++++++++++++++ src/Commands/stubs/field.stub | 52 ++++++++++++++++++++++ src/Commands/stubs/mutation.stub | 66 ++++++++++++++++++++++++++++ src/Commands/stubs/query.stub | 42 ++++++++++++++++++ src/Commands/stubs/type.stub | 55 +++++++++++++++++++++++ src/ServiceProvider.php | 4 ++ 10 files changed, 448 insertions(+), 9 deletions(-) create mode 100644 src/Commands/FieldMakeCommand.php create mode 100644 src/Commands/MutationMakeCommand.php create mode 100644 src/Commands/QueryMakeCommand.php create mode 100644 src/Commands/TypeMakeCommand.php create mode 100644 src/Commands/stubs/field.stub create mode 100644 src/Commands/stubs/mutation.stub create mode 100644 src/Commands/stubs/query.stub create mode 100644 src/Commands/stubs/type.stub diff --git a/config/config.php b/config/config.php index ef90e6e..c07e18c 100644 --- a/config/config.php +++ b/config/config.php @@ -1,15 +1,31 @@ [ + 'mutations' => 'App\\GraphQL\\Mutations', + 'queries' => 'App\\GraphQL\\Queries', + 'types' => 'App\\GraphQL\\Types', + 'fields' => 'App\\GraphQL\\Fields' + ], + + /* + |-------------------------------------------------------------------------- + | Schema declaration + |-------------------------------------------------------------------------- + | + | You can utilize this file to register all of you GraphQL schma queries + | and mutations. You can group collections together by namespace or middlware. + | + */ 'schema' => function () { Relay::group(['namespace' => 'Nuwave\\Relay'], function () { Relay::group(['namespace' => 'Node'], function () { diff --git a/src/Commands/FieldMakeCommand.php b/src/Commands/FieldMakeCommand.php new file mode 100644 index 0000000..be2650b --- /dev/null +++ b/src/Commands/FieldMakeCommand.php @@ -0,0 +1,51 @@ + '' + ]; + + /** + * The return type of the field. + * + * @return Type + */ + public function type() + { + // return GraphQL::type('type'); + } + + /** + * Available field arguments. + * + * @return array + */ + public function args() + { + return []; + } + + /** + * Resolve the field. + * + * @param mixed $root + * @param array $args + * @return mixed + */ + public function resolve($root, array $args) + { + // TODO: Resolve field + } +} diff --git a/src/Commands/stubs/mutation.stub b/src/Commands/stubs/mutation.stub new file mode 100644 index 0000000..4fb41b7 --- /dev/null +++ b/src/Commands/stubs/mutation.stub @@ -0,0 +1,66 @@ + '', + 'description' => '', + ]; + + /** + * Get model by id. + * + * When the root 'node' query is called, it will use this method + * to resolve the type by providing the id. + * + * @param string $id + * @return \Eloquence\Database\Model + */ + public function resolveById($id) + { + // return Model::find($id); + } + + /** + * Available fields of Type. + * + * @return array + */ + public function relayFields() + { + return []; + } + + /** + * List of related connections. + * + * @return array + */ + public function connections() + { + return []; + } +} diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 24e6278..fe85090 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -30,6 +30,10 @@ public function register() { $this->commands([ \Nuwave\Relay\Commands\SchemaCommand::class, + \Nuwave\Relay\Commands\MutationMakeCommand::class, + \Nuwave\Relay\Commands\FieldMakeCommand::class, + \Nuwave\Relay\Commands\QueryMakeCommand::class, + \Nuwave\Relay\Commands\TypeMakeCommand::class, ]); $this->app->singleton('relay', function ($app) { From 342c19900c1bd39b143feee8ae56357c7d69957e Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Fri, 12 Feb 2016 13:36:13 -0700 Subject: [PATCH 10/66] fix mutation make name --- src/Commands/MutationMakeCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/MutationMakeCommand.php b/src/Commands/MutationMakeCommand.php index 18ca27f..fd6196e 100644 --- a/src/Commands/MutationMakeCommand.php +++ b/src/Commands/MutationMakeCommand.php @@ -5,7 +5,7 @@ use Illuminate\Console\Command; use Illuminate\Console\GeneratorCommand; -class RelayMutationCommand extends GeneratorCommand +class MutationMakeCommand extends GeneratorCommand { /** * The name and signature of the console command. From c3db5cb8110010ffd0912bb7876b514929d2f271 Mon Sep 17 00:00:00 2001 From: Brandon Carroll Date: Thu, 25 Feb 2016 11:27:30 -0500 Subject: [PATCH 11/66] Update composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 52d36d4..9b68afc 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ } }, "require": { - "folklore/graphql": "0.*", + "folklore/graphql": "*", "illuminate/console": "5.*" } } From fca716a633813332df6cee6d9620dc02e15bd2f9 Mon Sep 17 00:00:00 2001 From: Brandon Carroll Date: Thu, 25 Feb 2016 20:40:02 -0500 Subject: [PATCH 12/66] Working on connection types --- config/config.php | 26 +- src/Commands/FieldMakeCommand.php | 3 +- src/Commands/MutationMakeCommand.php | 3 +- src/Commands/QueryMakeCommand.php | 3 +- src/Commands/SchemaCommand.php | 4 +- src/Commands/TypeMakeCommand.php | 3 +- src/Commands/stubs/field.stub | 2 +- src/Controllers/LumenController.php | 28 ++ src/Controllers/RelayController.php | 8 +- src/LaravelServiceProvider.php | 96 +++++++ src/LumenServiceProvider.php | 94 +++++++ src/Mutations/MutationWithClientId.php | 2 +- src/Node/NodeQuery.php | 17 +- src/Schema/SchemaContainer.php | 37 ++- src/ServiceProvider.php | 78 ------ src/{ => Traits}/GlobalIdTrait.php | 2 +- src/{ => Traits}/RelayModelTrait.php | 5 +- src/Types/ConnectionType.php | 218 +++++++++++++++ src/Types/EdgeType.php | 95 +++++++ src/Types/PageInfoType.php | 38 ++- src/Types/RelayType.php | 368 +++++-------------------- 21 files changed, 697 insertions(+), 433 deletions(-) create mode 100644 src/Controllers/LumenController.php create mode 100644 src/LaravelServiceProvider.php create mode 100644 src/LumenServiceProvider.php delete mode 100644 src/ServiceProvider.php rename src/{ => Traits}/GlobalIdTrait.php (96%) rename src/{ => Traits}/RelayModelTrait.php (73%) create mode 100644 src/Types/ConnectionType.php create mode 100644 src/Types/EdgeType.php diff --git a/config/config.php b/config/config.php index c07e18c..4ed0a42 100644 --- a/config/config.php +++ b/config/config.php @@ -1,20 +1,23 @@ [ 'mutations' => 'App\\GraphQL\\Mutations', 'queries' => 'App\\GraphQL\\Queries', 'types' => 'App\\GraphQL\\Types', - 'fields' => 'App\\GraphQL\\Fields' + 'fields' => 'App\\GraphQL\\Fields', ], /* @@ -22,20 +25,13 @@ | Schema declaration |-------------------------------------------------------------------------- | - | You can utilize this file to register all of you GraphQL schma queries - | and mutations. You can group collections together by namespace or middlware. + | This is a path that points to where your Relay schema is located + | relative to the app path. You should define your entire Relay + | schema in this file. Declare any Relay queries, mutations, + | and types here instead of laravel-graphql config file. | */ - 'schema' => function () { - Relay::group(['namespace' => 'Nuwave\\Relay'], function () { - Relay::group(['namespace' => 'Node'], function () { - Relay::query('node', 'NodeQuery'); - Relay::type('node', 'NodeType'); - }); - Relay::type('pageInfo', 'Types\\PageInfoType'); - }); + 'schema_path' => 'Http/GraphQL/schema.php', - // Additional Queries, Mutations and Types... - } ]; diff --git a/src/Commands/FieldMakeCommand.php b/src/Commands/FieldMakeCommand.php index be2650b..0fe5163 100644 --- a/src/Commands/FieldMakeCommand.php +++ b/src/Commands/FieldMakeCommand.php @@ -2,7 +2,6 @@ namespace Nuwave\Relay\Commands; -use Illuminate\Console\Command; use Illuminate\Console\GeneratorCommand; class FieldMakeCommand extends GeneratorCommand @@ -19,7 +18,7 @@ class FieldMakeCommand extends GeneratorCommand * * @var string */ - protected $description = 'Generate a graphql field.'; + protected $description = 'Generate a GraphQL/Relay field.'; /** * The type of class being generated. diff --git a/src/Commands/MutationMakeCommand.php b/src/Commands/MutationMakeCommand.php index fd6196e..aa4b2e8 100644 --- a/src/Commands/MutationMakeCommand.php +++ b/src/Commands/MutationMakeCommand.php @@ -2,7 +2,6 @@ namespace Nuwave\Relay\Commands; -use Illuminate\Console\Command; use Illuminate\Console\GeneratorCommand; class MutationMakeCommand extends GeneratorCommand @@ -19,7 +18,7 @@ class MutationMakeCommand extends GeneratorCommand * * @var string */ - protected $description = 'Generate a relay mutation.'; + protected $description = 'Generate a GraphQL/Relay mutation.'; /** * The type of class being generated. diff --git a/src/Commands/QueryMakeCommand.php b/src/Commands/QueryMakeCommand.php index dc236b9..f720a23 100644 --- a/src/Commands/QueryMakeCommand.php +++ b/src/Commands/QueryMakeCommand.php @@ -2,7 +2,6 @@ namespace Nuwave\Relay\Commands; -use Illuminate\Console\Command; use Illuminate\Console\GeneratorCommand; class QueryMakeCommand extends GeneratorCommand @@ -19,7 +18,7 @@ class QueryMakeCommand extends GeneratorCommand * * @var string */ - protected $description = 'Generate a relay query.'; + protected $description = 'Generate a GraphQL/Relay query.'; /** * The type of class being generated. diff --git a/src/Commands/SchemaCommand.php b/src/Commands/SchemaCommand.php index dde41a2..e7ba6cc 100644 --- a/src/Commands/SchemaCommand.php +++ b/src/Commands/SchemaCommand.php @@ -19,7 +19,7 @@ class SchemaCommand extends Command * * @var string */ - protected $description = 'Generate a new GraphQL schema.'; + protected $description = 'Generate a new Relay schema.'; /** * Relay schema generator. @@ -31,7 +31,7 @@ class SchemaCommand extends Command /** * Create a new command instance. * - * @return void + * @param SchemaGenerator $generator */ public function __construct(SchemaGenerator $generator) { diff --git a/src/Commands/TypeMakeCommand.php b/src/Commands/TypeMakeCommand.php index 4a4f968..6388e9d 100644 --- a/src/Commands/TypeMakeCommand.php +++ b/src/Commands/TypeMakeCommand.php @@ -2,7 +2,6 @@ namespace Nuwave\Relay\Commands; -use Illuminate\Console\Command; use Illuminate\Console\GeneratorCommand; class TypeMakeCommand extends GeneratorCommand @@ -19,7 +18,7 @@ class TypeMakeCommand extends GeneratorCommand * * @var string */ - protected $description = 'Generate a relay type.'; + protected $description = 'Generate a GraphQL/Relay type.'; /** * The type of class being generated. diff --git a/src/Commands/stubs/field.stub b/src/Commands/stubs/field.stub index d61e1ed..b441920 100644 --- a/src/Commands/stubs/field.stub +++ b/src/Commands/stubs/field.stub @@ -5,7 +5,7 @@ namespace DummyNamespace; use GraphQL; use GraphQL\Type\Definition\Type; use Folklore\GraphQL\Support\Field; -use Nuwave\Relay\GlobalIdTrait; +use Nuwave\Relay\Traits\GlobalIdTrait; class DummyClass extends Field { diff --git a/src/Controllers/LumenController.php b/src/Controllers/LumenController.php new file mode 100644 index 0000000..889e98d --- /dev/null +++ b/src/Controllers/LumenController.php @@ -0,0 +1,28 @@ +get('query'); + + $params = $request->get('variables'); + + if (is_string($params)) { + $params = json_decode($params, true); + } + + return app('graphql')->query($query, $params); + } +} diff --git a/src/Controllers/RelayController.php b/src/Controllers/RelayController.php index 44c8a02..ab1e887 100644 --- a/src/Controllers/RelayController.php +++ b/src/Controllers/RelayController.php @@ -2,20 +2,20 @@ namespace Nuwave\Relay\Controllers; -use Illuminate\Routing\Controller; use Illuminate\Http\Request; +use Illuminate\Routing\Controller; -class RelayController extends Controller +class LaravelController extends Controller { /** - * Excecute GraphQL query. + * Execute GraphQL query. * * @param Request $request * @return Response */ public function query(Request $request) { - $query = $request->get('query'); + $query = $request->get('query'); $params = $request->get('variables'); if (is_string($params)) { diff --git a/src/LaravelServiceProvider.php b/src/LaravelServiceProvider.php new file mode 100644 index 0000000..ec40691 --- /dev/null +++ b/src/LaravelServiceProvider.php @@ -0,0 +1,96 @@ +publishes([__DIR__ . '/../config/config.php' => config_path('relay.php')]); + + $this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'relay'); + + $this->registerSchema(); + } + + /** + * Register any application services. + * + * @return void + */ + public function register() + { + $this->commands([ + SchemaCommand::class, + MutationMakeCommand::class, + FieldMakeCommand::class, + QueryMakeCommand::class, + TypeMakeCommand::class, + ]); + + $this->app->singleton('graphql', function ($app) { + return new GraphQL($app); + }); + + $this->app->singleton('relay', function ($app) { + return new SchemaContainer(new Parser); + }); + } + + /** + * Register schema mutations and queries. + * + * @return void + */ + protected function registerSchema() + { + require_once app_path(config('relay.schema_path')); + + $this->setGraphQLConfig(); + + $this->initializeTypes(); + } + + /** + * Set GraphQL configuration variables. + * + * @return void + */ + protected function setGraphQLConfig() + { + $relay = $this->app['relay']; + + config([ + 'graphql.schema.mutation' => $relay->getMutations()->config(), + 'graphql.schema.query' => $relay->getQueries()->config(), + 'graphql.types' => $relay->getTypes()->config(), + ]); + } + + /** + * Initialize GraphQL types array. + * + * @return void + */ + protected function initializeTypes() + { + foreach(config('graphql.types') as $name => $type) { + $this->app['graphql']->addType($type, $name); + } + } +} diff --git a/src/LumenServiceProvider.php b/src/LumenServiceProvider.php new file mode 100644 index 0000000..e26c2b2 --- /dev/null +++ b/src/LumenServiceProvider.php @@ -0,0 +1,94 @@ +mergeConfigFrom(__DIR__ . '/../config/config.php', 'relay'); + + $this->registerSchema(); + } + + /** + * Register any application services. + * + * @return void + */ + public function register() + { + $this->commands([ + SchemaCommand::class, + MutationMakeCommand::class, + FieldMakeCommand::class, + QueryMakeCommand::class, + TypeMakeCommand::class, + ]); + + $this->app->singleton('graphql', function ($app) { + return new GraphQL($app); + }); + + $this->app->singleton('relay', function ($app) { + return new SchemaContainer(new Parser); + }); + } + + /** + * Register schema mutations and queries. + * + * @return void + */ + protected function registerSchema() + { + require_once __DIR__ . '/../../../../app/' . config('relay.schema_path'); + + $this->setGraphQLConfig(); + + $this->initializeTypes(); + } + + /** + * Set GraphQL configuration variables. + * + * @return void + */ + protected function setGraphQLConfig() + { + $relay = $this->app['relay']; + + config([ + 'graphql.schema.mutation' => $relay->getMutations()->config(), + 'graphql.schema.query' => $relay->getQueries()->config(), + 'graphql.types' => $relay->getTypes()->config(), + ]); + } + + /** + * Initialize GraphQL types array. + * + * @return void + */ + protected function initializeTypes() + { + foreach(config('graphql.types') as $name => $type) { + $this->app['graphql']->addType($type, $name); + } + } +} diff --git a/src/Mutations/MutationWithClientId.php b/src/Mutations/MutationWithClientId.php index 39a60a5..2e73535 100644 --- a/src/Mutations/MutationWithClientId.php +++ b/src/Mutations/MutationWithClientId.php @@ -4,7 +4,7 @@ use Validator; use Folklore\GraphQL\Error\ValidationError; -use Nuwave\Relay\GlobalIdTrait; +use Nuwave\Relay\Traits\GlobalIdTrait; use Folklore\GraphQL\Support\Mutation; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\ObjectType; diff --git a/src/Node/NodeQuery.php b/src/Node/NodeQuery.php index 3992b27..c81119d 100644 --- a/src/Node/NodeQuery.php +++ b/src/Node/NodeQuery.php @@ -3,7 +3,7 @@ namespace Nuwave\Relay\Node; use GraphQL; -use Nuwave\Relay\GlobalIdTrait; +use Nuwave\Relay\Traits\GlobalIdTrait; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\ResolveInfo; use Folklore\GraphQL\Support\Query; @@ -45,28 +45,15 @@ public function args() * @return Illuminate\Database\Eloquent\Model|array */ public function resolve($root, array $args, ResolveInfo $info) - { - return $this->getModel($args); - } - - /** - * Get associated model. - * - * @param array $args - * @return Illuminate\Database\Eloquent\Model - */ - protected function getModel(array $args) { // Here, we decode the base64 id and get the id of the type // as well as the type's name. - // list($typeClass, $id) = $this->decodeGlobalId($args['id']); - // Types must be registered in the graphql.php config file. - // foreach (config('graphql.types') as $type => $class) { if ($typeClass == $class) { $objectType = app($typeClass); + $model = $objectType->resolveById($id); if (is_array($model)) { diff --git a/src/Schema/SchemaContainer.php b/src/Schema/SchemaContainer.php index dd37332..8a19485 100644 --- a/src/Schema/SchemaContainer.php +++ b/src/Schema/SchemaContainer.php @@ -3,11 +3,9 @@ namespace Nuwave\Relay\Schema; use Closure; -use Illuminate\Http\Request; -use Nuwave\Relay\Schema\FieldCollection as Collection; -use Nuwave\Relay\Schema\Field; -use GraphQL\Language\Source; use GraphQL\Language\Parser as GraphQLParser; +use GraphQL\Language\Source; +use Nuwave\Relay\Schema\FieldCollection as Collection; class SchemaContainer { @@ -70,7 +68,7 @@ class SchemaContainer /** * Create new instance of Mutation container. * - * @return void + * @param Parser $parser */ public function __construct(Parser $parser) { @@ -170,11 +168,34 @@ protected function hasPath(Connection $connection, $parent) return preg_match("/{$parent}./", $connection->path); } + /** + * Add connection to collection. + * + * @param string $name + * @param string $namespace + * @return Field + */ + public function connection($name, $namespace) + { + $edgeType = $this->createField($name.'Edge', $namespace); + + $this->types->push($edgeType); + + $connectionType = $this->createField($name.'Connection', $namespace); + + $this->types->push($connectionType); + + return [ + 'connectionType' => $connectionType, + 'edgeType' => $edgeType, + ]; + } + /** * Add mutation to collection. * * @param string $name - * @param array $options + * @param array $namespace * @return Field */ public function mutation($name, $namespace) @@ -190,7 +211,7 @@ public function mutation($name, $namespace) * Add query to collection. * * @param string $name - * @param array $options + * @param array $namespace * @return Field */ public function query($name, $namespace) @@ -221,7 +242,7 @@ public function type($name, $namespace) /** * Group child elements. * - * @param array $middleware + * @param array $attributes * @param Closure $callback * @return void */ diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php deleted file mode 100644 index 235ccf7..0000000 --- a/src/ServiceProvider.php +++ /dev/null @@ -1,78 +0,0 @@ -publishes([__DIR__ . '/../config/config.php' => config_path('relay.php')]); - $this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'relay'); - - $this->registerSchema(); - $this->setConfig(); - } - - /** - * Register any application services. - * - * @return void - */ - public function register() - { - $this->commands([ - \Nuwave\Relay\Commands\SchemaCommand::class, - \Nuwave\Relay\Commands\MutationMakeCommand::class, - \Nuwave\Relay\Commands\FieldMakeCommand::class, - \Nuwave\Relay\Commands\QueryMakeCommand::class, - \Nuwave\Relay\Commands\TypeMakeCommand::class, - ]); - - $this->app->singleton('relay', function ($app) { - return new SchemaContainer(new Parser); - }); - - $this->app->alias('relay', SchemaContainer::class); - } - - /** - * Register schema mutations and queries. - * - * @return void - */ - protected function registerSchema() - { - $register = config('relay.schema'); - - $register(); - } - - /** - * Set configuration variables. - * - * @return void - */ - protected function setConfig() - { - $schema = $this->app['relay']; - - $mutations = config('graphql.schema.mutation', []); - $queries = config('graphql.schema.query', []); - $types = config('graphql.types', []); - - config([ - 'graphql.schema.mutation' => array_merge($mutations, $schema->getMutations()->config()), - 'graphql.schema.query' => array_merge($queries, $schema->getQueries()->config()), - 'graphql.types' => array_merge($types, $schema->getTypes()->config()) - ]); - } -} diff --git a/src/GlobalIdTrait.php b/src/Traits/GlobalIdTrait.php similarity index 96% rename from src/GlobalIdTrait.php rename to src/Traits/GlobalIdTrait.php index 8f852b9..866be0f 100644 --- a/src/GlobalIdTrait.php +++ b/src/Traits/GlobalIdTrait.php @@ -1,6 +1,6 @@ attributes[$this->getKeyName()]; } diff --git a/src/Types/ConnectionType.php b/src/Types/ConnectionType.php new file mode 100644 index 0000000..e51a8a4 --- /dev/null +++ b/src/Types/ConnectionType.php @@ -0,0 +1,218 @@ + [ + 'type' => Type::nonNull(GraphQL::type('pageInfo')), + 'description' => 'Information to aid in pagination.', + 'resolve' => function ($collection) { + return $collection; + }, + ], + 'edges' => [ + 'type' => Type::listOf($this->buildEdgeType($this->name, $this->type())), + 'description' => 'Information to aid in pagination.', + 'resolve' => function ($collection) { + return $this->injectCursor($collection); + }, + ] + ]; + } + + /** + * Get the default arguments for a connection. + * + * @return array + */ + public static function connectionArgs() + { + return [ + 'after' => [ + 'type' => Type::string() + ], + 'first' => [ + 'type' => Type::int() + ], + 'before' => [ + 'type' => Type::string() + ], + 'last' => [ + 'type' => Type::int() + ] + ]; + } + + /** + * Build the edge type for this connection. + * + * @param $name + * @param $type + * @return ObjectType + */ + protected function buildEdgeType($name, $type) + { + $edge = new EdgeType($name, $type); + + return $edge->toType(); + } + + /** + * Inject encoded cursor into collection items. + * + * @param mixed $collection + * @return mixed + */ + protected function injectCursor($collection) + { + if ($collection instanceof LengthAwarePaginator) { + $page = $collection->currentPage(); + + $collection->each(function ($item, $x) use ($page) { + $cursor = ($x + 1) * $page; + $encodedCursor = $this->encodeGlobalId('arrayconnection', $cursor); + if (is_array($item)) { + $item['relayCursor'] = $encodedCursor; + } else { + if (is_object($item) && is_array($item->attributes)) { + $item->attributes['relayCursor'] = $encodedCursor; + } else { + $item->relayCursor = $encodedCursor; + } + } + }); + } + + return $collection; + } + + /** + * Get id from encoded cursor. + * + * @param string $cursor + * @return integer + */ + protected function getCursorId($cursor) + { + return (int)$this->decodeRelayId($cursor); + } + + /** + * Convert the Fluent instance to an array. + * + * @return array + */ + public function toArray() + { + $fields = array_merge($this->baseFields(), $this->fields()); + + return [ + 'name' => ucfirst($this->name), + 'description' => 'A connection to a list of items.', + 'fields' => $fields, + 'resolve' => function ($root, $args, ResolveInfo $info) { + return $this->resolve($root, $args, $info, $this->name); + } + ]; + } + + /** + * Create the instance of the connection type. + * + * @param Closure $pageInfoResolver + * @param Closure $edgeResolver + * @return ObjectType + */ + public function toType(Closure $pageInfoResolver = null, Closure $edgeResolver = null) + { + $this->pageInfoResolver = $pageInfoResolver; + + $this->edgeResolver = $edgeResolver; + + return new ObjectType($this->toArray()); + } + + /** + * Dynamically retrieve the value of an attribute. + * + * @param string $key + * @return mixed + */ + public function __get($key) + { + $attributes = $this->getAttributes(); + + return isset($attributes[$key]) ? $attributes[$key] : null; + } + + /** + * Dynamically check if an attribute is set. + * + * @param string $key + * @return boolean + */ + public function __isset($key) + { + return isset($this->getAttributes()[$key]); + } + + /** + * Get the type of nodes at the end of this connection. + * + * @return mixed + */ + abstract public function type(); +} \ No newline at end of file diff --git a/src/Types/EdgeType.php b/src/Types/EdgeType.php new file mode 100644 index 0000000..e0ca7eb --- /dev/null +++ b/src/Types/EdgeType.php @@ -0,0 +1,95 @@ +name = $name; + $this->type = $type; + } + + /** + * Fields that exist on every connection. + * + * @return array + */ + public function fields() + { + return [ + 'node' => [ + 'type' => $this->type, + 'description' => 'The item at the end of the edge.', + 'resolve' => function ($edge) { + return $edge; + }, + ], + 'cursor' => [ + 'type' => Type::nonNull(Type::string()), + 'description' => 'A cursor for use in pagination.', + 'resolve' => function ($edge) { + if (is_array($edge) && isset($edge['relayCursor'])) { + return $edge['relayCursor']; + } elseif (is_array($edge->attributes)) { + return $edge->attributes['relayCursor']; + } + + return $edge->relayCursor; + }, + ] + ]; + } + + /** + * Convert the Fluent instance to an array. + * + * @return array + */ + public function toArray() + { + return [ + 'name' => ucfirst($this->name).'Edge', + 'description' => 'An edge in a connection.', + 'fields' => $this->fields(), + ]; + } + + /** + * Create the instance of the connection type. + * + * @return ObjectType + */ + public function toType() + { + return new ObjectType($this->toArray()); + } +} \ No newline at end of file diff --git a/src/Types/PageInfoType.php b/src/Types/PageInfoType.php index f0d7abe..3460caf 100644 --- a/src/Types/PageInfoType.php +++ b/src/Types/PageInfoType.php @@ -6,17 +6,21 @@ use GraphQL\Type\Definition\ResolveInfo; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Folklore\GraphQL\Support\Type as GraphQLType; +use Nuwave\Relay\Traits\GlobalIdTrait; class PageInfoType extends GraphQLType { + + use GlobalIdTrait; + /** * Attributes of PageInfo. * * @var array */ protected $attributes = [ - 'name' => 'PageInfo', - 'description' => 'Information about pagination in a connection.' + 'name' => 'pageInfo', + 'description' => 'Information to aid in pagination.' ]; /** @@ -30,7 +34,7 @@ public function fields() 'hasNextPage' => [ 'type' => Type::nonNull(Type::boolean()), 'description' => 'When paginating forwards, are there more items?', - 'resolve' => function ($collection, $test) { + 'resolve' => function ($collection) { if ($collection instanceof LengthAwarePaginator) { return $collection->hasMorePages(); } @@ -48,6 +52,34 @@ public function fields() return false; } + ], + 'startCursor' => [ + 'type' => Type::string(), + 'description' => 'When paginating backwards, the cursor to continue.', + 'resolve' => function ($collection) { + if ($collection instanceof LengthAwarePaginator) { + return $this->encodeGlobalId( + 'arrayconnection', + $collection->firstItem() * $collection->currentPage() + ); + } + + return null; + } + ], + 'endCursor' => [ + 'type' => Type::string(), + 'description' => 'When paginating forwards, the cursor to continue.', + 'resolve' => function ($collection) { + if ($collection instanceof LengthAwarePaginator) { + return $this->encodeGlobalId( + 'arrayconnection', + $collection->lastItem() * $collection->currentPage() + ); + } + + return null; + } ] ]; } diff --git a/src/Types/RelayType.php b/src/Types/RelayType.php index b53ddab..3b92110 100644 --- a/src/Types/RelayType.php +++ b/src/Types/RelayType.php @@ -2,31 +2,19 @@ namespace Nuwave\Relay\Types; -use Closure; use GraphQL; -use GraphQL\Language\AST\ListType; -use Folklore\GraphQL\Support\Type as GraphQLType; -use GraphQL\Type\Definition\Type; -use GraphQL\Type\Definition\ObjectType; -use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ResolveInfo; -use Illuminate\Contracts\Pagination\LengthAwarePaginator; -use Illuminate\Pagination\LengthAwarePaginator as Paginator; +use GraphQL\Type\Definition\Type; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Collection; -use Nuwave\Relay\GlobalIdTrait; -use Nuwave\Relay\Schema\SchemaContainer; +use Nuwave\Relay\Traits\GlobalIdTrait; +use Illuminate\Pagination\LengthAwarePaginator as Paginator; -abstract class RelayType extends GraphQLType + +abstract class RelayType extends \Folklore\GraphQL\Support\Type { - use GlobalIdTrait; - /** - * Schema container. - * - * @var SchemaContainer - */ - protected $container; + use GlobalIdTrait; /** * List of fields with global identifier. @@ -37,9 +25,9 @@ public function fields() { return array_merge($this->relayFields(), $this->getConnections(), [ 'id' => [ - 'type' => Type::nonNull(Type::id()), + 'type' => Type::nonNull(Type::id()), 'description' => 'ID of type.', - 'resolve' => function ($obj) { + 'resolve' => function ($obj) { return $this->encodeGlobalId(get_called_class(), $this->getIdentifier($obj)); }, ], @@ -47,24 +35,13 @@ public function fields() } /** - * Get the identifier of the type. - * - * @param mixed $obj - * @return mixed - */ - public function getIdentifier($obj) - { - return $obj->id; - } - - /** - * List of available interfaces. + * Available connections for type. * * @return array */ - public function interfaces() + protected function connections() { - return [GraphQL::type('node')]; + return []; } /** @@ -74,259 +51,77 @@ public function interfaces() */ public function getConnections() { - $edges = []; - - foreach ($this->connections() as $name => $edge) { - $injectCursor = isset($edge['injectCursor']) ? $edge['injectCursor'] : null; - $resolveCursor = isset($edge['resolveCursor']) ? $edge['resolveCursor'] : null; - - $edgeType = $this->edgeType($name, $edge['type'], $resolveCursor); - $connectionType = $this->connectionType($name, Type::listOf($edgeType), $injectCursor); - - $edges[$name] = [ - 'type' => $connectionType, - 'description' => 'A connection to a list of items.', - 'args' => [ - 'first' => [ - 'name' => 'first', - 'type' => Type::int() - ], - 'after' => [ - 'name' => 'after', - 'type' => Type::string() - ] - ], - 'resolve' => function ($parent, array $args, ResolveInfo $info) use ($name, $edge) { - $collection = isset($edge['resolve']) ? $edge['resolve']($parent, $args, $info) : $this->autoResolve($parent, $args, $info, $name); - - if (isset($edge['load'])) { - $this->autoLoad($collection, $edge, $name); - } - - return $collection; + return collect($this->connections())->transform(function ($edge, $name) { + $edge['resolve'] = function ($collection, array $args, ResolveInfo $info) use ($name) { + $items = $this->getItems($collection, $info, $name); + + if (isset($args['first'])) { + $total = $items->count(); + $first = $args['first']; + $after = $this->decodeCursor($args); + $currentPage = $first && $after ? floor(($first + $after) / $first) : 1; + + return new Paginator( + $items->slice($after)->take($first), + $total, + $first, + $currentPage + ); } - ]; - } - return $edges; - } + return new Paginator( + $items, + count($items), + count($items) + ); + }; - /** - * Auto resolve the connection. - * - * @param mixed $parent - * @param array $args - * @param ResolveInfo $info - * @param string $name - * @return Paginator - */ - protected function autoResolve($parent, array $args, ResolveInfo $info, $name = '') - { - $items = []; + $edge['args'] = ConnectionType::connectionArgs(); - if ($parent instanceof Model) { - $items = $parent->getAttribute($name); - } else if (is_object($parent) && method_exists($parent, 'get')) { - $items = $parent->get($name); - } else if (is_array($parent) && isset($parent[$name])) { - $items = new Collection($parent[$name]); - } - - if (isset($args['first'])) { - $total = $items->count(); - $first = $args['first']; - $after = $this->decodeCursor($args); - $currentPage = $first && $after ? floor(($first + $after) / $first) : 1; - - return new Paginator( - $items->slice($after)->take($first), - $total, - $first, - $currentPage - ); - } + return $edge; - return new Paginator( - $items, - count($items), - count($items) - ); + })->toArray(); } /** - * Auto load the fields connection(s). - * - * @param Paginator $collection - * @param array $edge - * @param string $name - * @return void + * @param $collection + * @param ResolveInfo $info + * @param $name + * @return mixed|Collection */ - protected function autoLoad(Paginator $collection, array $edge, $name) + protected function getItems($collection, ResolveInfo $info, $name) { - $relay = $this->getContainer(); - - if ($relay->isParent($name)) { - if ($typeClass = $this->typeFromSchema($edge['type'])) { - $type = app($typeClass); - $connections = $relay->connectionsInRequest($name, $type->connections()); - - foreach ($connections as $key => $connection) { - if (isset($connection['load'])) { - $load = $connection['load']; - - $load($collection, $relay->connectionArguments($key)); - } - } - } - } - } - - /** - * Generate PageInfo object type. - * - * @return ObjectType - */ - protected function pageInfoType() - { - return GraphQL::type('pageInfo'); - } - - /** - * Generate EdgeType. - * - * @param string $name - * @param mixed $type - * @return ObjectType - */ - protected function edgeType($name, $type, Closure $resolveCursor = null) - { - if ($type instanceof ListOfType) { - $type = $type->getWrappedType(); - } - - return new ObjectType([ - 'name' => ucfirst($name) . 'Edge', - 'fields' => [ - 'node' => [ - 'type' => $type, - 'description' => 'The item at the end of the edge.', - 'resolve' => function ($edge, array $args, ResolveInfo $info) { - return $edge; - } - ], - 'cursor' => [ - 'type' => Type::nonNull(Type::string()), - 'description' => 'A cursor for use in pagination.', - 'resolve' => function ($edge, array $args, ResolveInfo $info) use ($resolveCursor) { - if ($resolveCursor) { - return $resolveCursor($edge, $args, $info); - } - - return $this->resolveCursor($edge); - } - ] - ] - ]); - } - - /** - * Create ConnectionType. - * - * @param string $name - * @param mixed $type - * @return ObjectType - */ - protected function connectionType($name, $type, Closure $injectCursor = null) - { - if (!$type instanceof ListOfType) { - $type = Type::listOf($type); - } - - return new ObjectType([ - 'name' => ucfirst($name) . 'Connection', - 'fields' => [ - 'edges' => [ - 'type' => $type, - 'resolve' => function ($collection, array $args, ResolveInfo $info) use ($injectCursor) { - if ($injectCursor) { - return $injectCursor($collection, $args, $info); - } - - return $this->injectCursor($collection); - } - ], - 'pageInfo' => [ - 'type' => Type::nonNull($this->pageInfoType()), - 'description' => 'Information to aid in pagination.', - 'resolve' => function ($collection, array $args, ResolveInfo $info) { - return $collection; - } - ] - ] - ]); - } - - /** - * Inject encoded cursor into collection items. - * - * @param mixed $collection - * @return mixed - */ - protected function injectCursor($collection) - { - if ($collection instanceof LengthAwarePaginator) { - $page = $collection->currentPage(); - - foreach ($collection as $x => &$item) { - $cursor = ($x + 1) * $page; - $encodedCursor = $this->encodeGlobalId('arrayconnection', $cursor); + $items = []; - if (is_array($item)) { - $item['relayCursor'] = $encodedCursor; - } else if (is_object($item) && is_array($item->attributes)) { - $item->attributes['relayCursor'] = $encodedCursor; - } else { - $item->relayCursor = $encodedCursor; - } - } + if ($collection instanceof Model) { + // Selects only the fields requested, instead of select * + $items = method_exists($collection, $name) + ? $collection->$name()->select(...$this->getSelectFields($info))->get() + : $collection->getAttribute($name); + return $items; + } elseif (is_object($collection) && method_exists($collection, 'get')) { + $items = $collection->get($name); + return $items; + } elseif (is_array($collection) && isset($collection[$name])) { + $items = new Collection($collection[$name]); + return $items; } - return $collection; + return $items; } /** - * Resolve encoded relay cursor for item. + * Select only certain fields on queries instead of all fields. * - * @param mixed $edge - * @return string - */ - protected function resolveCursor($edge) - { - if (is_array($edge) && isset($edge['relayCursor'])) { - return $edge['relayCursor']; - } elseif (is_array($edge->attributes)) { - return $edge->attributes['relayCursor']; - } - - return $edge->relayCursor; - } - - /** - * Resolve type from schema. - * - * @param ListOfType|Type $edge - * @return string|null + * @param ResolveInfo $info + * @return array */ - protected function typeFromSchema($edge) + protected function getSelectFields(ResolveInfo $info) { - $type = $edge instanceof ListOfType ? $edge->getWrappedType() : $edge; - $schema = config('graphql.types'); - - if ($typeClass = array_get($schema, $type->toString())) { - return $typeClass; - } - - return array_get($schema, strtolower($type->toString())); + return collect($info->getFieldSelection(4)['edges']['node']) + ->reject(function ($value) { + is_array($value); + })->keys()->toArray(); } /** @@ -341,40 +136,24 @@ public function decodeCursor(array $args) } /** - * Get id from encoded cursor. - * - * @param string $cursor - * @return integer - */ - protected function getCursorId($cursor) - { - return (int)$this->decodeRelayId($cursor); - } - - /** - * Get schema container instance. + * Get the identifier of the type. * - * @return SchemaContainer + * @param \Illuminate\Database\Eloquent\Model $obj + * @return mixed */ - protected function getContainer() + public function getIdentifier(Model $obj) { - if ($this->container) { - return $this->container; - } - - $this->container = app('relay'); - - return $this->container; + return $obj->id; } /** - * Available connections for type. + * List of available interfaces. * * @return array */ - protected function connections() + public function interfaces() { - return []; + return [GraphQL::type('node')]; } /** @@ -387,7 +166,8 @@ abstract protected function relayFields(); /** * Fetch type data by id. * - * @param string $id + * @param string $id + * * @return mixed */ abstract public function resolveById($id); From 0101e5052b2a644022070f3b57c05a073efbb844 Mon Sep 17 00:00:00 2001 From: Brandon Carroll Date: Thu, 25 Feb 2016 20:46:15 -0500 Subject: [PATCH 13/66] default schema --- src/LaravelServiceProvider.php | 18 ++++++++++++++++++ src/LumenServiceProvider.php | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/LaravelServiceProvider.php b/src/LaravelServiceProvider.php index ec40691..139ed41 100644 --- a/src/LaravelServiceProvider.php +++ b/src/LaravelServiceProvider.php @@ -63,9 +63,27 @@ protected function registerSchema() $this->setGraphQLConfig(); + $this->registerRelayTypes(); + $this->initializeTypes(); } + /** + * Register the default relay types in the schema. + * + * @return void + */ + protected function registerRelayTypes() + { + $relay = $this->app['relay']; + + $relay->group(['namespace' => 'Nuwave\\Relay'], function () use ($relay) { + $relay->query('node', 'Node\\NodeQuery'); + $relay->type('node', 'Node\\NodeType'); + $relay->type('pageInfo', 'Types\\PageInfoType'); + }); + } + /** * Set GraphQL configuration variables. * diff --git a/src/LumenServiceProvider.php b/src/LumenServiceProvider.php index e26c2b2..9c6b69c 100644 --- a/src/LumenServiceProvider.php +++ b/src/LumenServiceProvider.php @@ -57,6 +57,8 @@ public function register() */ protected function registerSchema() { + $this->registerRelayTypes(); + require_once __DIR__ . '/../../../../app/' . config('relay.schema_path'); $this->setGraphQLConfig(); @@ -64,6 +66,22 @@ protected function registerSchema() $this->initializeTypes(); } + /** + * Register the default relay types in the schema. + * + * @return void + */ + protected function registerRelayTypes() + { + $relay = $this->app['relay']; + + $relay->group(['namespace' => 'Nuwave\\Relay'], function () use ($relay) { + $relay->query('node', 'Node\\NodeQuery'); + $relay->type('node', 'Node\\NodeType'); + $relay->type('pageInfo', 'Types\\PageInfoType'); + }); + } + /** * Set GraphQL configuration variables. * From 7f4b7d5e89eeaf85fcc57ba5bb093affab73a1b8 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 27 Feb 2016 06:16:23 -0700 Subject: [PATCH 14/66] remove connection for name GraphQL/Relay naming convention: https://facebook.github.io/relay/graphql/connections.htm (2.2 Introspection) --- src/Types/ConnectionType.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Types/ConnectionType.php b/src/Types/ConnectionType.php index e51a8a4..83d2a37 100644 --- a/src/Types/ConnectionType.php +++ b/src/Types/ConnectionType.php @@ -105,6 +105,10 @@ public static function connectionArgs() */ protected function buildEdgeType($name, $type) { + if (preg_match('/Connection$/', $name)) { + $name = substr($name, 0, strlen($name) - 10); + } + $edge = new EdgeType($name, $type); return $edge->toType(); @@ -215,4 +219,4 @@ public function __isset($key) * @return mixed */ abstract public function type(); -} \ No newline at end of file +} From 66e31c06900fb592d1f43b5751006270dc3a3c9f Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 27 Feb 2016 07:40:27 -0700 Subject: [PATCH 15/66] removed eloquent model dependency We won't always be resolving a eloquent model here. For example, we may be resolving data from a 3rd party API --- src/Types/RelayType.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Types/RelayType.php b/src/Types/RelayType.php index 3b92110..83af08b 100644 --- a/src/Types/RelayType.php +++ b/src/Types/RelayType.php @@ -10,7 +10,6 @@ use Nuwave\Relay\Traits\GlobalIdTrait; use Illuminate\Pagination\LengthAwarePaginator as Paginator; - abstract class RelayType extends \Folklore\GraphQL\Support\Type { @@ -138,10 +137,10 @@ public function decodeCursor(array $args) /** * Get the identifier of the type. * - * @param \Illuminate\Database\Eloquent\Model $obj + * @param mixed $obj * @return mixed */ - public function getIdentifier(Model $obj) + public function getIdentifier($obj) { return $obj->id; } From 723e715734c3bad01c1e4cd5477a7d665d73da42 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 27 Feb 2016 07:40:45 -0700 Subject: [PATCH 16/66] use new LaravelServiceProvider --- tests/BaseTest.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/BaseTest.php b/tests/BaseTest.php index 046672b..c2f9983 100644 --- a/tests/BaseTest.php +++ b/tests/BaseTest.php @@ -33,7 +33,7 @@ protected function getPackageProviders($app) { return [ \Folklore\GraphQL\GraphQLServiceProvider::class, - \Nuwave\Relay\ServiceProvider::class, + \Nuwave\Relay\LaravelServiceProvider::class, ]; } @@ -47,6 +47,7 @@ protected function getPackageAliases($app) { return [ 'GraphQL' => \Folklore\GraphQL\Support\Facades\GraphQL::class, + 'Relay' => \Nuwave\Relay\LaravelServiceProvider::class, ]; } @@ -78,5 +79,9 @@ protected function getEnvironmentSetUp($app) 'human' => \Nuwave\Relay\Tests\Assets\Types\HumanType::class, ] ]); + + $app['config']->set('relay', [ + 'schema_path' => 'schema/schema.php' + ]); } } From f5373b75cfead2b8ff1140e805073eb755d8c932 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 27 Feb 2016 07:41:29 -0700 Subject: [PATCH 17/66] merge config w/ graphql --- src/LaravelServiceProvider.php | 12 ++++++++---- src/LumenServiceProvider.php | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/LaravelServiceProvider.php b/src/LaravelServiceProvider.php index 139ed41..5d15cfd 100644 --- a/src/LaravelServiceProvider.php +++ b/src/LaravelServiceProvider.php @@ -93,10 +93,14 @@ protected function setGraphQLConfig() { $relay = $this->app['relay']; + $mutations = config('graphql.schema.mutation', []); + $queries = config('graphql.schema.query', []); + $types = config('graphql.types', []); + config([ - 'graphql.schema.mutation' => $relay->getMutations()->config(), - 'graphql.schema.query' => $relay->getQueries()->config(), - 'graphql.types' => $relay->getTypes()->config(), + 'graphql.schema.mutation' => array_merge($mutations, $relay->getMutations()->config()), + 'graphql.schema.query' => array_merge($queries, $relay->getQueries()->config()), + 'graphql.types' => array_merge($types, $relay->getTypes()->config()) ]); } @@ -107,7 +111,7 @@ protected function setGraphQLConfig() */ protected function initializeTypes() { - foreach(config('graphql.types') as $name => $type) { + foreach (config('graphql.types') as $name => $type) { $this->app['graphql']->addType($type, $name); } } diff --git a/src/LumenServiceProvider.php b/src/LumenServiceProvider.php index 9c6b69c..816f889 100644 --- a/src/LumenServiceProvider.php +++ b/src/LumenServiceProvider.php @@ -91,10 +91,14 @@ protected function setGraphQLConfig() { $relay = $this->app['relay']; + $mutations = config('graphql.schema.mutation', []); + $queries = config('graphql.schema.query', []); + $types = config('graphql.types', []); + config([ - 'graphql.schema.mutation' => $relay->getMutations()->config(), - 'graphql.schema.query' => $relay->getQueries()->config(), - 'graphql.types' => $relay->getTypes()->config(), + 'graphql.schema.mutation' => array_merge($mutations, $relay->getMutations()->config()), + 'graphql.schema.query' => array_merge($queries, $relay->getQueries()->config()), + 'graphql.types' => array_merge($types, $relay->getTypes()->config()) ]); } @@ -105,7 +109,7 @@ protected function setGraphQLConfig() */ protected function initializeTypes() { - foreach(config('graphql.types') as $name => $type) { + foreach (config('graphql.types') as $name => $type) { $this->app['graphql']->addType($type, $name); } } From 2933af34394c9df35c9c46c777f19770cd379d55 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 27 Feb 2016 07:54:47 -0700 Subject: [PATCH 18/66] don't override set resolve function --- src/Types/RelayType.php | 42 +++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/Types/RelayType.php b/src/Types/RelayType.php index 83af08b..305df6f 100644 --- a/src/Types/RelayType.php +++ b/src/Types/RelayType.php @@ -51,29 +51,31 @@ protected function connections() public function getConnections() { return collect($this->connections())->transform(function ($edge, $name) { - $edge['resolve'] = function ($collection, array $args, ResolveInfo $info) use ($name) { - $items = $this->getItems($collection, $info, $name); - - if (isset($args['first'])) { - $total = $items->count(); - $first = $args['first']; - $after = $this->decodeCursor($args); - $currentPage = $first && $after ? floor(($first + $after) / $first) : 1; + if (!isset($edge['resolve'])) { + $edge['resolve'] = function ($collection, array $args, ResolveInfo $info) use ($name) { + $items = $this->getItems($collection, $info, $name); + + if (isset($args['first'])) { + $total = $items->count(); + $first = $args['first']; + $after = $this->decodeCursor($args); + $currentPage = $first && $after ? floor(($first + $after) / $first) : 1; + + return new Paginator( + $items->slice($after)->take($first), + $total, + $first, + $currentPage + ); + } return new Paginator( - $items->slice($after)->take($first), - $total, - $first, - $currentPage + $items, + count($items), + count($items) ); - } - - return new Paginator( - $items, - count($items), - count($items) - ); - }; + }; + } $edge['args'] = ConnectionType::connectionArgs(); From 0b7bd04d22db5af385b62ec77a5e26d5f7ffba27 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 27 Feb 2016 08:02:15 -0700 Subject: [PATCH 19/66] re-add getCursorId --- src/Types/RelayType.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Types/RelayType.php b/src/Types/RelayType.php index 305df6f..2952d36 100644 --- a/src/Types/RelayType.php +++ b/src/Types/RelayType.php @@ -136,6 +136,17 @@ public function decodeCursor(array $args) return isset($args['after']) ? $this->getCursorId($args['after']) : 0; } + /** + * Get id from encoded cursor. + * + * @param string $cursor + * @return integer + */ + protected function getCursorId($cursor) + { + return (int)$this->decodeRelayId($cursor); + } + /** * Get the identifier of the type. * From 6dae5341dfa3337224ce56819ee73ff163e427d4 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sun, 28 Feb 2016 08:59:06 -0700 Subject: [PATCH 20/66] created GraphQL container --- src/Schema/GraphQL.php | 235 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 src/Schema/GraphQL.php diff --git a/src/Schema/GraphQL.php b/src/Schema/GraphQL.php new file mode 100644 index 0000000..b1d5ec9 --- /dev/null +++ b/src/Schema/GraphQL.php @@ -0,0 +1,235 @@ +app = $app; + + $this->types = collect(); + $this->queries = collect(); + $this->mutations = collect(); + $this->typeInstances = collect(); + } + + /** + * Execute GraphQL query. + * + * @param string $query + * @param array $variables + * @return array + */ + public function query($query, $variables = []) + { + $result = $this->queryAndReturnResult($query, $variables); + + if (!empty($result->errors)) + { + return [ + 'data' => $result->data, + 'errors' => array_map([$this, 'formatError'], $result->errors) + ]; + } + + return ['data' => $result->data]; + } + + /** + * Execute GraphQL query. + * + * @param string $query + * @param array $variables + * @return array + */ + public function queryAndReturnResult($query, $variables = []) + { + return GraphQLBase::executeAndReturnResult($this->schema(), $query, null, $variables); + } + + /** + * Generate GraphQL Schema. + * + * @return \GraphQL\Schema + */ + public function schema() + { + $schema = config('graphql.schema'); + + $this->types->each(function ($type, $key) { + $this->type($key); + }); + + $queries = $this->queries->merge(array_get($schema, 'query', [])); + $mutations = $this->mutations->merge(array_get($schema, 'mutation', [])); + + $queryTypes = $this->generateType($queries, ['name' => 'Query']); + $mutationTypes = $this->generateType($mutations, ['name' => 'Mutation']); + + return new Schema($queryTypes, $mutationTypes); + } + + /** + * Generate type from collection of fields. + * + * @param Collection $fields + * @param array $options + * @return \GraphQL\Type\Definition\ObjectType + */ + public function generateType(Collection $fields, $options = []) + { + $typeFields = $fields->transform(function ($field) { + if (is_string($field)) { + return app($field)->toArray(); + } + + return $field; + })->toArray(); + + return new ObjectType(array_merge(['fields' => $typeFields], $options)); + } + + /** + * Add mutation to collection. + * + * @param string $name + * @param mixed $mutator + */ + public function addMutation($name, $mutator) + { + $this->mutations->put($name, $mutator); + } + + /** + * Add query to collection. + * + * @param string $name + * @param mixed $query + */ + public function addQuery($name, $query) + { + $this->queries->put($name, $query); + } + + /** + * Add type to collection. + * + * @param mixed $class + * @param string|null $name + */ + public function addType($class, $name = null) + { + if (!$name) { + $type = is_object($class) ? $class : app($class); + $name = $type->name; + } + + $this->types->put($name, $class); + } + + /** + * Get instance of type. + * + * @param string $name + * @param boolean $fresh + * @return mixed + */ + public function type($name, $fresh = false) + { + if (!$this->types->has($name)) { + throw new \Exception("Type [{$name}] not found."); + } + + if (!$fresh && $this->typeInstances->has($name)) { + return $this->typeInstances->get($name); + } + + $type = $this->types->get($name); + if (!is_object($type)) { + $type = app($type); + } + + $instance = $type->toType(); + $this->typeInstances->put($name, $instance); + + if ($type->interfaces) { + InterfaceType::addImplementationToInterfaces($instance); + } + + return $instance; + } + + /** + * Format error for output. + * + * @param Error $e + * @return array + */ + public function formatError(Error $e) + { + $error = ['message' => $e->getMessage()]; + + $locations = $e->getLocations(); + if (!empty($locations)) { + $error['locations'] = array_map(function ($location) { + return $location->toArray(); + }, $locations); + } + + $previous = $e->getPrevious(); + if ($previous && $previous instanceof ValidationError) { + $error['validation'] = $previous->getValidatorMessage(); + } + + return $error; + } +} From d1b9f1c0475f98d0b0554e04b1883540b2f91427 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sun, 28 Feb 2016 15:21:47 -0700 Subject: [PATCH 21/66] created eloquent type --- src/Schema/GraphQL.php | 5 +- src/Types/EloquentType.php | 151 +++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/Types/EloquentType.php diff --git a/src/Schema/GraphQL.php b/src/Schema/GraphQL.php index b1d5ec9..a527847 100644 --- a/src/Schema/GraphQL.php +++ b/src/Schema/GraphQL.php @@ -9,6 +9,8 @@ use GraphQL\Type\Definition\InterfaceType; use Folklore\GraphQL\Error\ValidationError; use Illuminate\Support\Collection; +use Illuminate\Database\Eloquent\Model; +use Nuwave\Relay\Types\EloquentType; class GraphQL { @@ -198,7 +200,8 @@ public function type($name, $fresh = false) $type = app($type); } - $instance = $type->toType(); + $instance = $type instanceof Model ? (new EloquentType($type))->toType() : $type->toType(); + $this->typeInstances->put($name, $instance); if ($type->interfaces) { diff --git a/src/Types/EloquentType.php b/src/Types/EloquentType.php new file mode 100644 index 0000000..b0adecf --- /dev/null +++ b/src/Types/EloquentType.php @@ -0,0 +1,151 @@ +fields = collect(); + $this->hiddenFields = collect($model->getHidden()); + $this->model = $model; + } + + /** + * Transform eloquent model to graphql type. + * + * @return \GraphQL\Type\Definition\ObjectType + */ + public function toType() + { + $this->createFields(); + + $config = []; + $config['name'] = $this->model->name ?: ucfirst((new ReflectionClass($this->model))->getShortName()); + $config['$description'] = isset($this->model->description) ? $this->model->description : null; + $config['fields'] = $this->fields->toArray(); + + return new ObjectType($config); + } + + /** + * Create fields for type. + * + * @return void + */ + protected function createFields() + { + $table = $this->model->getTable(); + $schema = $this->model->getConnection()->getSchemaBuilder(); + $columns = collect($schema->getColumnListing($table)); + + $columns->each(function ($column) use ($table, $schema) { + if (!$this->skipAutoGenerate($column)) { + $this->generateField( + $column, + $schema->getColumnType($table, $column) + ); + } + }); + } + + /** + * Generate type field from schema. + * + * @param string $name + * @param string $colType + * @return void + */ + protected function generateField($name, $colType) + { + $field = []; + $field['type'] = $this->resolveTypeByColumn($name, $colType); + $field['description'] = isset($this->descriptions['name']) ? $this->descriptions[$name] : null; + + if ($name === $this->model->getKeyName()) { + $field['description'] = $field['description'] ?: 'Primary id of type.'; + } + + $resolve = 'resolve' . ucfirst(camel_case($name)); + + if (method_exists($this->model, $resolve)) { + $field['resolve'] = function ($root) use ($resolve) { + return $this->model->{$resolve}($root); + }; + } + + $fieldName = $this->model->camelCase ? camel_case($name) : $name; + + $this->fields->put($fieldName, $field); + } + + /** + * Resolve field type by column info. + * + * @param string $name + * @param string $colType + * @return \GraphQL\Type\Definition\Type + */ + protected function resolveTypeByColumn($name, $colType) + { + $type = Type::string(); + + if ($name === $this->model->getKeyName()) { + $field['type'] = Type::nonNull(Type::id()); + } elseif ($type === 'integer') { + $field['type'] = Type::int(); + } elseif ($type === 'float' || $type === 'decimal') { + $field['type'] = Type::float(); + } elseif ($type === 'boolean') { + $field['type'] = Type::boolean(); + } + + return $type; + } + + /** + * Check if field should be skipped. + * + * @param string $field + * @return boolean + */ + protected function skipAutoGenerate($name = '') + { + if ($this->hiddenFields->has($name)) { + return true; + } + + return false; + } +} From 49648140878055d55479930eee3e6831b201c22b Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Mon, 29 Feb 2016 15:37:01 -0700 Subject: [PATCH 22/66] use fields generated in model if available --- src/Types/EloquentType.php | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/Types/EloquentType.php b/src/Types/EloquentType.php index b0adecf..c11bdc2 100644 --- a/src/Types/EloquentType.php +++ b/src/Types/EloquentType.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\ResolveInfo; class EloquentType { @@ -49,7 +50,11 @@ public function __construct(Model $model) */ public function toType() { - $this->createFields(); + if (method_exists($this->model, 'graphqlFields')) { + $this->eloquentFields(); + } else { + $this->schemaFields(); + } $config = []; $config['name'] = $this->model->name ?: ucfirst((new ReflectionClass($this->model))->getShortName()); @@ -59,12 +64,40 @@ public function toType() return new ObjectType($config); } + /** + * Convert eloquent defined fields. + * + * @return array + */ + public function eloquentFields() + { + $fields = collect($this->model->graphqlFields()); + + $fields->each(function ($field, $key) { + $method = 'resolve' . studly_case($key) . 'Field'; + + $data = []; + $data['type'] = $field['type']; + $data['description'] = isset($field['description']) ? $field['description'] : null; + + if (isset($field['resolve'])) { + $data['resolve'] = $field['resolve']; + } elseif (method_exists($this->model, $method)) { + $data['resolve'] = function ($root, $args, $info) use ($method) { + return $this->model->{$method}($root, $args, $info); + }; + } + + $this->fields->put($key, $data); + }); + } + /** * Create fields for type. * * @return void */ - protected function createFields() + protected function schemaFields() { $table = $this->model->getTable(); $schema = $this->model->getConnection()->getSchemaBuilder(); From da3a1feab71469096611f9d947cc51343f631959 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Thu, 3 Mar 2016 07:03:11 -0700 Subject: [PATCH 23/66] generate raw fields for use in stub --- src/Types/EloquentType.php | 94 ++++++++++++++++++++++++++++---------- 1 file changed, 71 insertions(+), 23 deletions(-) diff --git a/src/Types/EloquentType.php b/src/Types/EloquentType.php index c11bdc2..af22afa 100644 --- a/src/Types/EloquentType.php +++ b/src/Types/EloquentType.php @@ -39,7 +39,7 @@ class EloquentType public function __construct(Model $model) { $this->fields = collect(); - $this->hiddenFields = collect($model->getHidden()); + $this->hiddenFields = collect($model->getHidden())->flip(); $this->model = $model; } @@ -64,6 +64,26 @@ public function toType() return new ObjectType($config); } + /** + * Get fields for model. + * + * @return \Illuminate\Support\Collection + */ + public function rawFields() + { + if (method_exists($this->model, 'graphqlFields')) { + $this->eloquentFields(); + } else { + $this->schemaFields(); + } + + return $this->fields->transform(function ($field, $key) { + $field['type'] = $this->getRawType($field['type']); + + return $field; + }); + } + /** * Convert eloquent defined fields. * @@ -74,21 +94,23 @@ public function eloquentFields() $fields = collect($this->model->graphqlFields()); $fields->each(function ($field, $key) { - $method = 'resolve' . studly_case($key) . 'Field'; - - $data = []; - $data['type'] = $field['type']; - $data['description'] = isset($field['description']) ? $field['description'] : null; - - if (isset($field['resolve'])) { - $data['resolve'] = $field['resolve']; - } elseif (method_exists($this->model, $method)) { - $data['resolve'] = function ($root, $args, $info) use ($method) { - return $this->model->{$method}($root, $args, $info); - }; + if (!$this->skipAutoGenerate($key)) { + $method = 'resolve' . studly_case($key) . 'Field'; + + $data = []; + $data['type'] = $field['type']; + $data['description'] = isset($field['description']) ? $field['description'] : null; + + if (isset($field['resolve'])) { + $data['resolve'] = $field['resolve']; + } elseif (method_exists($this->model, $method)) { + $data['resolve'] = function ($root, $args, $info) use ($method) { + return $this->model->{$method}($root, $args, $info); + }; + } + + $this->fields->put($key, $data); } - - $this->fields->put($key, $data); }); } @@ -130,7 +152,7 @@ protected function generateField($name, $colType) $field['description'] = $field['description'] ?: 'Primary id of type.'; } - $resolve = 'resolve' . ucfirst(camel_case($name)); + $resolve = 'resolve' . studly_case($name); if (method_exists($this->model, $resolve)) { $field['resolve'] = function ($root) use ($resolve) { @@ -155,18 +177,44 @@ protected function resolveTypeByColumn($name, $colType) $type = Type::string(); if ($name === $this->model->getKeyName()) { - $field['type'] = Type::nonNull(Type::id()); - } elseif ($type === 'integer') { - $field['type'] = Type::int(); - } elseif ($type === 'float' || $type === 'decimal') { - $field['type'] = Type::float(); - } elseif ($type === 'boolean') { - $field['type'] = Type::boolean(); + $type = Type::nonNull(Type::id()); + } elseif ($colType === 'integer') { + $type = Type::int(); + } elseif ($colType === 'float' || $colType === 'decimal') { + $type = Type::float(); + } elseif ($colType === 'boolean') { + $type = Type::boolean(); } return $type; } + /** + * Get raw name for type. + * + * @param Type $type + * @return string + */ + protected function getRawType(Type $type) + { + $class = get_class($type); + $namespace = 'GraphQL\\Type\\Definition\\'; + + if ($class == $namespace . 'NonNull') { + return 'Type::nonNull('. $this->getRawType($type->getWrappedType()) .')'; + } elseif ($class == $namespace . 'IDType') { + return 'Type::id()'; + } elseif ($class == $namespace . 'IntType') { + return 'Type::int()'; + } elseif ($class == $namespace . 'BooleanType') { + return 'Type::bool()'; + } elseif ($class == $namespace . 'FloatType') { + return 'Type::float()'; + } + + return 'Type::string()'; + } + /** * Check if field should be skipped. * From dc9faa95d979974604fb7fa5683bfe1589676d0c Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Fri, 4 Mar 2016 07:22:09 -0700 Subject: [PATCH 24/66] add model option to type make command --- config/config.php | 2 + src/Commands/TypeMakeCommand.php | 81 +++++++++++++++++++++++++++ src/Commands/stubs/eloquent.blade.php | 51 +++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 src/Commands/stubs/eloquent.blade.php diff --git a/config/config.php b/config/config.php index 4ed0a42..8ccc82c 100644 --- a/config/config.php +++ b/config/config.php @@ -34,4 +34,6 @@ 'schema_path' => 'Http/GraphQL/schema.php', + 'model_path' => 'App\\Models', + ]; diff --git a/src/Commands/TypeMakeCommand.php b/src/Commands/TypeMakeCommand.php index 6388e9d..8564bd9 100644 --- a/src/Commands/TypeMakeCommand.php +++ b/src/Commands/TypeMakeCommand.php @@ -2,7 +2,10 @@ namespace Nuwave\Relay\Commands; +use ReflectionClass; +use Nuwave\Relay\Types\EloquentType; use Illuminate\Console\GeneratorCommand; +use Symfony\Component\Console\Input\InputOption; class TypeMakeCommand extends GeneratorCommand { @@ -47,4 +50,82 @@ protected function getDefaultNamespace($rootNamespace) { return config('relay.namespaces.types'); } + + /** + * Build the class with the given name. + * + * @param string $name + * @return string + */ + protected function buildClass($name) + { + if ($model = $this->option('model')) { + $this->setViewPath(); + $stub = $this->getEloquentStub($model); + } else { + $stub = $this->files->get($this->getStub()); + } + + return $this->replaceNamespace($stub, $name)->replaceClass($stub, $name); + } + + /** + * Get the console command options. + * + * @return array + */ + protected function getOptions() + { + return [ + ['model', null, InputOption::VALUE_OPTIONAL, 'Generate a Eloquent GraphQL type.'], + ]; + } + + /** + * Set config view paths. + * + * @return void + */ + protected function setViewPath() + { + $paths = config('view.paths'); + $paths[] = realpath(__DIR__.'/stubs'); + + config(['view.paths' => $paths]); + } + + /** + * Generate stub from eloquent type. + * + * @param string $model + * @return string + */ + protected function getEloquentStub($model) + { + $shortName = $model; + $rootNamespace = $this->laravel->getNamespace(); + + if (starts_with($model, $rootNamespace)) { + $shortName = (new ReflectionClass($model))->getShortName(); + } else { + $model = config('relay.model_path') . "\\" . $model; + } + + $fields = $this->getTypeFields($model); + + return "render(); + } + + /** + * Generate fields for type. + * + * @param string $class + * @return array + */ + protected function getTypeFields($class) + { + $model = app($class); + + return (new EloquentType($model))->rawFields(); + } } diff --git a/src/Commands/stubs/eloquent.blade.php b/src/Commands/stubs/eloquent.blade.php new file mode 100644 index 0000000..9139d65 --- /dev/null +++ b/src/Commands/stubs/eloquent.blade.php @@ -0,0 +1,51 @@ +namespace DummyNamespace; + +use GraphQL; +use GraphQL\Type\Definition\Type; +use Nuwave\Relay\Types\RelayType; +use GraphQL\Type\Definition\ResolveInfo; +use {{ $model }}; + +class DummyClass extends RelayType +{ + /** + * Attributes of Type. + * + * @var array + */ + protected $attributes = [ + 'name' => '{{ $shortName }}', + 'description' => '', + ]; + + /** + * Get customer by id. + * + * When the root 'node' query is called, it will use this method + * to resolve the type by providing the id. + * + * @param string $id + * @return User + */ + public function resolveById($id) + { + return {{ $shortName }}::findOrFail($id); + } + + /** + * Available fields of Type. + * + * @return array + */ + public function relayFields() + { + return [ +@foreach($fields as $key => $field) + '{{ $key }}' => [ + 'type' => {{ $field['type'] }}, + 'description' => '{{ $field['description'] }}', + ], +@endforeach + ]; + } +} From 083de1b62aa0674d877bae9c56544592e646b80c Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Fri, 4 Mar 2016 10:50:56 -0700 Subject: [PATCH 25/66] add connection method to GraphQL --- src/Schema/GraphQL.php | 135 +++++++++++++++++++++++++++-- src/Support/ConnectionResolver.php | 117 +++++++++++++++++++++++++ src/Types/RelayType.php | 88 +------------------ 3 files changed, 248 insertions(+), 92 deletions(-) create mode 100644 src/Support/ConnectionResolver.php diff --git a/src/Schema/GraphQL.php b/src/Schema/GraphQL.php index a527847..af412a3 100644 --- a/src/Schema/GraphQL.php +++ b/src/Schema/GraphQL.php @@ -2,15 +2,21 @@ namespace Nuwave\Relay\Schema; +use Closure; use GraphQL\GraphQL as GraphQLBase; use GraphQL\Error; use GraphQL\Schema; use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\InterfaceType; use Folklore\GraphQL\Error\ValidationError; +use Folklore\GraphQL\Support\Field; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Model; +use Nuwave\Relay\Types\RelayType; use Nuwave\Relay\Types\EloquentType; +use Nuwave\Relay\Types\ConnectionType; +use Nuwave\Relay\Support\ConnectionResolver; class GraphQL { @@ -29,11 +35,11 @@ class GraphQL protected $queries; /** - * Collection of registered types. + * Collection of registered mutations. * * @var \Illuminate\Support\Collection */ - protected $types; + protected $mutations; /** * Collection of type instances. @@ -43,11 +49,18 @@ class GraphQL protected $typeInstances; /** - * Collection of registered mutations. + * Collection of connection type instances. * * @var \Illuminate\Support\Collection */ - protected $mutations; + protected $connectionInstances; + + /** + * Instance of connection resolver. + * + * @var ConnectionResolver + */ + protected $connectionResolver; /** * Create a new instance of GraphQL. @@ -62,6 +75,7 @@ public function __construct($app) $this->queries = collect(); $this->mutations = collect(); $this->typeInstances = collect(); + $this->connectionInstances = collect(); } /** @@ -187,9 +201,7 @@ public function addType($class, $name = null) */ public function type($name, $fresh = false) { - if (!$this->types->has($name)) { - throw new \Exception("Type [{$name}] not found."); - } + $this->checkType($name); if (!$fresh && $this->typeInstances->has($name)) { return $this->typeInstances->get($name); @@ -211,6 +223,38 @@ public function type($name, $fresh = false) return $instance; } + /** + * Get instance of connection type. + * + * @param string $name + * @param Closure|string|null $resolve + * @param boolean $fresh + * @return mixed + */ + public function connection($name, $resolve = null, $fresh = false) + { + $this->checkType($name); + + if ($resolve && !$resolve instanceof Closure) { + $resolve = function ($root, array $args, ResolveInfo $info) use ($resolve) { + return $this->resolveConnection($root, $args, $info, $resolve); + }; + } + + if (!$fresh && $this->connectionInstances->has($name)) { + $field = $this->connectionInstances->get($name); + $field['resolve'] = $resolve; + + return $field; + } + + $field = $this->connectionField($name, $resolve); + + $this->connectionInstances->put($name, $field); + + return $field; + } + /** * Format error for output. * @@ -235,4 +279,81 @@ public function formatError(Error $e) return $error; } + + /** + * Check if type is registered. + * + * @param string $name + * @return void + */ + protected function checkType($name) + { + if (!$this->types->has($name)) { + throw new \Exception("Type [{$name}] not found."); + } + } + + /** + * Generate connection field. + * + * @param string $name + * @param Closure|null $resolve + * @return array + */ + public function connectionField($name, $resolve = null) + { + $edge = $this->type($name); + $type = new ConnectionType(); + $connectionName = (!preg_match('/Connection$/', $name)) ? $name.'Connection' : $name; + + $type->setName(studly_case($connectionName)); + $type->setEdgeType($edge); + $instance = $type->toType(); + + $field = [ + 'args' => ConnectionType::connectionArgs(), + 'type' => $instance, + 'resolve' => $resolve + ]; + + if ($type->interfaces) { + InterfaceType::addImplementationToInterfaces($instance); + } + + return $field; + } + + /** + * Auto-resolve connection. + * + * @param mixed $root + * @param array $args + * @param ResolveInfo $info + * @param string $name + * @return mixed + */ + public function resolveConnection($root, array $args, ResolveInfo $info, $name = '') + { + return $this->getConnectionResolver()->resolve($root, $args, $info, $name); + } + + /** + * Set instance of connection resolver. + * + * @param ConnectionResolver $resolver + */ + public function setConnectionResolver(ConnectionResolver $resolver) + { + $this->connectionResolver = $resolver; + } + + /** + * Get instance of connection resolver. + * + * @return ConnectionResolver + */ + public function getConnectionResolver() + { + return $this->connectionResolver ?: app(ConnectionResolver::class); + } } diff --git a/src/Support/ConnectionResolver.php b/src/Support/ConnectionResolver.php new file mode 100644 index 0000000..da4a941 --- /dev/null +++ b/src/Support/ConnectionResolver.php @@ -0,0 +1,117 @@ +getItems($root, $info, $name); + + if (isset($args['first'])) { + $total = $items->count(); + $first = $args['first']; + $after = $this->decodeCursor($args); + $currentPage = $first && $after ? floor(($first + $after) / $first) : 1; + + return new Paginator( + $items->slice($after)->take($first), + $total, + $first, + $currentPage + ); + } + + return new Paginator( + $items, + count($items), + count($items) + ); + } + + /** + * @param $collection + * @param ResolveInfo $info + * @param $name + * @return mixed|Collection + */ + protected function getItems($collection, ResolveInfo $info, $name) + { + $items = []; + + if ($collection instanceof Model) { + // Selects only the fields requested, instead of select * + $items = method_exists($collection, $name) + ? $collection->$name()->select(...$this->getSelectFields($info))->get() + : $collection->getAttribute($name); + return $items; + } elseif (is_object($collection) && method_exists($collection, 'get')) { + $items = $collection->get($name); + return $items; + } elseif (is_array($collection) && isset($collection[$name])) { + return collect($collection[$name]); + } + + return $items; + } + + /** + * Select only certain fields on queries instead of all fields. + * + * @param ResolveInfo $info + * @return array + */ + protected function getSelectFields(ResolveInfo $info) + { + $camel = config('relay.camel_case'); + + return collect($info->getFieldSelection(4)['edges']['node']) + ->reject(function ($value) { + is_array($value); + })->keys()->transform(function ($value) use ($camel) { + if ($camel) { + return snake_case($value); + } + + return $value; + })->toArray(); + } + + /** + * Decode cursor from query arguments. + * + * @param array $args + * @return integer + */ + public function decodeCursor(array $args) + { + return isset($args['after']) ? $this->getCursorId($args['after']) : 0; + } + + /** + * Get id from encoded cursor. + * + * @param string $cursor + * @return integer + */ + protected function getCursorId($cursor) + { + return (int)$this->decodeRelayId($cursor); + } +} diff --git a/src/Types/RelayType.php b/src/Types/RelayType.php index 2952d36..bfe79ea 100644 --- a/src/Types/RelayType.php +++ b/src/Types/RelayType.php @@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Model; use Nuwave\Relay\Traits\GlobalIdTrait; use Illuminate\Pagination\LengthAwarePaginator as Paginator; +use Illuminate\Contracts\Pagination\LengthAwarePaginator; abstract class RelayType extends \Folklore\GraphQL\Support\Type { @@ -52,28 +53,8 @@ public function getConnections() { return collect($this->connections())->transform(function ($edge, $name) { if (!isset($edge['resolve'])) { - $edge['resolve'] = function ($collection, array $args, ResolveInfo $info) use ($name) { - $items = $this->getItems($collection, $info, $name); - - if (isset($args['first'])) { - $total = $items->count(); - $first = $args['first']; - $after = $this->decodeCursor($args); - $currentPage = $first && $after ? floor(($first + $after) / $first) : 1; - - return new Paginator( - $items->slice($after)->take($first), - $total, - $first, - $currentPage - ); - } - - return new Paginator( - $items, - count($items), - count($items) - ); + $edge['resolve'] = function ($root, array $args, ResolveInfo $info) use ($name) { + return GraphQL::resolveConnection($root, $args, $info, $name); }; } @@ -84,69 +65,6 @@ public function getConnections() })->toArray(); } - /** - * @param $collection - * @param ResolveInfo $info - * @param $name - * @return mixed|Collection - */ - protected function getItems($collection, ResolveInfo $info, $name) - { - $items = []; - - if ($collection instanceof Model) { - // Selects only the fields requested, instead of select * - $items = method_exists($collection, $name) - ? $collection->$name()->select(...$this->getSelectFields($info))->get() - : $collection->getAttribute($name); - return $items; - } elseif (is_object($collection) && method_exists($collection, 'get')) { - $items = $collection->get($name); - return $items; - } elseif (is_array($collection) && isset($collection[$name])) { - $items = new Collection($collection[$name]); - return $items; - } - - return $items; - } - - /** - * Select only certain fields on queries instead of all fields. - * - * @param ResolveInfo $info - * @return array - */ - protected function getSelectFields(ResolveInfo $info) - { - return collect($info->getFieldSelection(4)['edges']['node']) - ->reject(function ($value) { - is_array($value); - })->keys()->toArray(); - } - - /** - * Decode cursor from query arguments. - * - * @param array $args - * @return integer - */ - public function decodeCursor(array $args) - { - return isset($args['after']) ? $this->getCursorId($args['after']) : 0; - } - - /** - * Get id from encoded cursor. - * - * @param string $cursor - * @return integer - */ - protected function getCursorId($cursor) - { - return (int)$this->decodeRelayId($cursor); - } - /** * Get the identifier of the type. * From 97dee77adeca2977dab8c86933b401b0a60c1a70 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Fri, 4 Mar 2016 11:32:07 -0700 Subject: [PATCH 26/66] remove field import --- src/Schema/GraphQL.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Schema/GraphQL.php b/src/Schema/GraphQL.php index af412a3..8f8c30f 100644 --- a/src/Schema/GraphQL.php +++ b/src/Schema/GraphQL.php @@ -10,7 +10,6 @@ use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\InterfaceType; use Folklore\GraphQL\Error\ValidationError; -use Folklore\GraphQL\Support\Field; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Model; use Nuwave\Relay\Types\RelayType; @@ -89,8 +88,7 @@ public function query($query, $variables = []) { $result = $this->queryAndReturnResult($query, $variables); - if (!empty($result->errors)) - { + if (!empty($result->errors)) { return [ 'data' => $result->data, 'errors' => array_map([$this, 'formatError'], $result->errors) From 28012b6273b818483f9a109636603fadf8784488 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Fri, 4 Mar 2016 11:35:05 -0700 Subject: [PATCH 27/66] swapped out GraphQL implementation --- config/config.php | 2 +- src/LaravelServiceProvider.php | 2 +- src/LumenServiceProvider.php | 2 +- src/Types/ConnectionType.php | 38 +++++++++++++++++++++++++++++++--- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/config/config.php b/config/config.php index 8ccc82c..40b2e0b 100644 --- a/config/config.php +++ b/config/config.php @@ -35,5 +35,5 @@ 'schema_path' => 'Http/GraphQL/schema.php', 'model_path' => 'App\\Models', - + 'camel_case' => false, ]; diff --git a/src/LaravelServiceProvider.php b/src/LaravelServiceProvider.php index 5d15cfd..a0598b1 100644 --- a/src/LaravelServiceProvider.php +++ b/src/LaravelServiceProvider.php @@ -2,7 +2,7 @@ namespace Nuwave\Relay; -use Folklore\GraphQL\GraphQL; +use Nuwave\Relay\Schema\GraphQL; use Nuwave\Relay\Commands\FieldMakeCommand; use Nuwave\Relay\Commands\MutationMakeCommand; use Nuwave\Relay\Commands\QueryMakeCommand; diff --git a/src/LumenServiceProvider.php b/src/LumenServiceProvider.php index 816f889..2abb732 100644 --- a/src/LumenServiceProvider.php +++ b/src/LumenServiceProvider.php @@ -2,7 +2,7 @@ namespace Nuwave\Relay; -use Folklore\GraphQL\GraphQL; +use Nuwave\Relay\Schema\GraphQL; use Nuwave\Relay\Commands\FieldMakeCommand; use Nuwave\Relay\Commands\MutationMakeCommand; use Nuwave\Relay\Commands\QueryMakeCommand; diff --git a/src/Types/ConnectionType.php b/src/Types/ConnectionType.php index 83d2a37..1636a89 100644 --- a/src/Types/ConnectionType.php +++ b/src/Types/ConnectionType.php @@ -11,11 +11,18 @@ use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Nuwave\Relay\Traits\GlobalIdTrait; -abstract class ConnectionType extends GraphQLType +class ConnectionType extends GraphQLType { use GlobalIdTrait; + /** + * Type of node at the end of this connection. + * + * @return mixed + */ + protected $type; + /** * The edge resolver for this connection type * @@ -55,6 +62,8 @@ public function fields() */ protected function baseFields() { + $type = $this->type ?: $this->type(); + return [ 'pageInfo' => [ 'type' => Type::nonNull(GraphQL::type('pageInfo')), @@ -64,7 +73,7 @@ protected function baseFields() }, ], 'edges' => [ - 'type' => Type::listOf($this->buildEdgeType($this->name, $this->type())), + 'type' => Type::listOf($this->buildEdgeType($this->name, $type)), 'description' => 'Information to aid in pagination.', 'resolve' => function ($collection) { return $this->injectCursor($collection); @@ -189,6 +198,26 @@ public function toType(Closure $pageInfoResolver = null, Closure $edgeResolver = return new ObjectType($this->toArray()); } + /** + * Set the type at the end of the connection. + * + * @param Type $type + */ + public function setEdgeType($type) + { + $this->type = $type; + } + + /** + * Set name of connection. + * + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + /** * Dynamically retrieve the value of an attribute. * @@ -218,5 +247,8 @@ public function __isset($key) * * @return mixed */ - abstract public function type(); + public function type() + { + return null; + } } From 9ac6e5489a63ae659ab1e137ba047817ef6b160e Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Fri, 4 Mar 2016 13:29:28 -0700 Subject: [PATCH 28/66] restructured folders --- src/Commands/stubs/eloquent.blade.php | 2 +- src/Commands/stubs/mutation.stub | 4 +- src/Commands/stubs/type.stub | 2 +- ...ayController.php => LaravelController.php} | 0 src/Facades/GraphQL.php | 18 ++ src/LaravelServiceProvider.php | 2 +- src/LumenServiceProvider.php | 2 +- src/Schema/GraphQL.php | 13 +- .../Definition}/EdgeType.php | 5 +- .../Definition}/EloquentType.php | 8 +- src/Support/Definition/GraphQLField.php | 128 ++++++++++++++ src/Support/Definition/GraphQLInterface.php | 48 ++++++ src/Support/Definition/GraphQLMutation.php | 115 +++++++++++++ src/Support/Definition/GraphQLQuery.php | 7 + src/Support/Definition/GraphQLType.php | 162 ++++++++++++++++++ .../Definition}/PageInfoType.php | 3 +- .../Definition/RelayConnectionType.php} | 5 +- .../Definition/RelayMutation.php} | 9 +- .../Definition}/RelayType.php | 8 +- src/{ => Support}/SchemaGenerator.php | 2 +- src/Support/ValidationError.php | 39 +++++ 21 files changed, 548 insertions(+), 34 deletions(-) rename src/Controllers/{RelayController.php => LaravelController.php} (100%) create mode 100644 src/Facades/GraphQL.php rename src/{Types => Support/Definition}/EdgeType.php (96%) rename src/{Types => Support/Definition}/EloquentType.php (96%) create mode 100644 src/Support/Definition/GraphQLField.php create mode 100644 src/Support/Definition/GraphQLInterface.php create mode 100644 src/Support/Definition/GraphQLMutation.php create mode 100644 src/Support/Definition/GraphQLQuery.php create mode 100644 src/Support/Definition/GraphQLType.php rename src/{Types => Support/Definition}/PageInfoType.php (97%) rename src/{Types/ConnectionType.php => Support/Definition/RelayConnectionType.php} (97%) rename src/{Mutations/MutationWithClientId.php => Support/Definition/RelayMutation.php} (96%) rename src/{Types => Support/Definition}/RelayType.php (93%) rename src/{ => Support}/SchemaGenerator.php (97%) create mode 100644 src/Support/ValidationError.php diff --git a/src/Commands/stubs/eloquent.blade.php b/src/Commands/stubs/eloquent.blade.php index 9139d65..defebba 100644 --- a/src/Commands/stubs/eloquent.blade.php +++ b/src/Commands/stubs/eloquent.blade.php @@ -2,7 +2,7 @@ use GraphQL; use GraphQL\Type\Definition\Type; -use Nuwave\Relay\Types\RelayType; +use Nuwave\Relay\Support\Definition\RelayType; use GraphQL\Type\Definition\ResolveInfo; use {{ $model }}; diff --git a/src/Commands/stubs/mutation.stub b/src/Commands/stubs/mutation.stub index 4fb41b7..90d0a83 100644 --- a/src/Commands/stubs/mutation.stub +++ b/src/Commands/stubs/mutation.stub @@ -6,9 +6,9 @@ use GraphQL; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\InputObjectType; -use Nuwave\Relay\Mutations\MutationWithClientId; +use Nuwave\Relay\Support\Definition\RelayMutation; -class DummyClass extends MutationWithClientId +class DummyClass extends RelayMutation { /** * Name of mutation. diff --git a/src/Commands/stubs/type.stub b/src/Commands/stubs/type.stub index 9f12372..6169638 100644 --- a/src/Commands/stubs/type.stub +++ b/src/Commands/stubs/type.stub @@ -4,7 +4,7 @@ namespace DummyNamespace; use GraphQL; use GraphQL\Type\Definition\Type; -use Nuwave\Relay\Types\RelayType; +use Nuwave\Relay\Support\Definition\RelayType; use GraphQL\Type\Definition\ResolveInfo; class DummyClass extends RelayType diff --git a/src/Controllers/RelayController.php b/src/Controllers/LaravelController.php similarity index 100% rename from src/Controllers/RelayController.php rename to src/Controllers/LaravelController.php diff --git a/src/Facades/GraphQL.php b/src/Facades/GraphQL.php new file mode 100644 index 0000000..87ce1c5 --- /dev/null +++ b/src/Facades/GraphQL.php @@ -0,0 +1,18 @@ +group(['namespace' => 'Nuwave\\Relay'], function () use ($relay) { $relay->query('node', 'Node\\NodeQuery'); $relay->type('node', 'Node\\NodeType'); - $relay->type('pageInfo', 'Types\\PageInfoType'); + $relay->type('pageInfo', 'Support\\Definition\\PageInfoType'); }); } diff --git a/src/LumenServiceProvider.php b/src/LumenServiceProvider.php index 2abb732..eb3a103 100644 --- a/src/LumenServiceProvider.php +++ b/src/LumenServiceProvider.php @@ -78,7 +78,7 @@ protected function registerRelayTypes() $relay->group(['namespace' => 'Nuwave\\Relay'], function () use ($relay) { $relay->query('node', 'Node\\NodeQuery'); $relay->type('node', 'Node\\NodeType'); - $relay->type('pageInfo', 'Types\\PageInfoType'); + $relay->type('pageInfo', 'Support\\Definition\\PageInfoType'); }); } diff --git a/src/Schema/GraphQL.php b/src/Schema/GraphQL.php index 8f8c30f..649ce26 100644 --- a/src/Schema/GraphQL.php +++ b/src/Schema/GraphQL.php @@ -9,12 +9,13 @@ use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\InterfaceType; -use Folklore\GraphQL\Error\ValidationError; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Model; -use Nuwave\Relay\Types\RelayType; -use Nuwave\Relay\Types\EloquentType; -use Nuwave\Relay\Types\ConnectionType; + +use Nuwave\Relay\Support\ValidationError; +use Nuwave\Relay\Support\Definition\RelayType; +use Nuwave\Relay\Support\Definition\EloquentType; +use Nuwave\Relay\Support\Definition\RelayConnectionType; use Nuwave\Relay\Support\ConnectionResolver; class GraphQL @@ -301,7 +302,7 @@ protected function checkType($name) public function connectionField($name, $resolve = null) { $edge = $this->type($name); - $type = new ConnectionType(); + $type = new RelayConnectionType(); $connectionName = (!preg_match('/Connection$/', $name)) ? $name.'Connection' : $name; $type->setName(studly_case($connectionName)); @@ -309,7 +310,7 @@ public function connectionField($name, $resolve = null) $instance = $type->toType(); $field = [ - 'args' => ConnectionType::connectionArgs(), + 'args' => RelayConnectionType::connectionArgs(), 'type' => $instance, 'resolve' => $resolve ]; diff --git a/src/Types/EdgeType.php b/src/Support/Definition/EdgeType.php similarity index 96% rename from src/Types/EdgeType.php rename to src/Support/Definition/EdgeType.php index e0ca7eb..536b5d7 100644 --- a/src/Types/EdgeType.php +++ b/src/Support/Definition/EdgeType.php @@ -1,10 +1,9 @@ toArray()); } -} \ No newline at end of file +} diff --git a/src/Types/EloquentType.php b/src/Support/Definition/EloquentType.php similarity index 96% rename from src/Types/EloquentType.php rename to src/Support/Definition/EloquentType.php index af22afa..10df2f4 100644 --- a/src/Types/EloquentType.php +++ b/src/Support/Definition/EloquentType.php @@ -1,6 +1,6 @@ graphQL = app('graphql'); + } + + /** + * Arguments this field accepts. + * + * @return array + */ + public function args() + { + return []; + } + + /** + * Field attributes. + * + * @return array + */ + public function attributes() + { + return []; + } + + /** + * The field type. + * + * @return \GraphQL\Type\Definition\ObjectType + */ + public function type() + { + return null; + } + + /** + * Get the attributes of the field. + * + * @return array + */ + public function getAttributes() + { + $attributes = array_merge($this->attributes, [ + 'args' => $this->args() + ], $this->attributes()); + + $attributes['type'] = $this->type(); + + $attributes['resolve'] = $this->getResolver(); + + return $attributes; + } + + /** + * Get the field resolver. + * + * @return \Closure|null + */ + protected function getResolver() + { + if(!method_exists($this, 'resolve')) { + return null; + } + + $resolver = array($this, 'resolve'); + + return function() use ($resolver) { + return call_user_func_array($resolver, func_get_args()); + }; + } + + /** + * Convert the Fluent instance to an array. + * + * @return array + */ + public function toArray() + { + return $this->getAttributes(); + } + + /** + * Dynamically retrieve the value of an attribute. + * + * @param string $key + * @return mixed + */ + public function __get($key) + { + $attributes = $this->getAttributes(); + + return isset($attributes[$key]) ? $attributes[$key]:null; + } + + /** + * Dynamically check if an attribute is set. + * + * @param string $key + * @return bool + */ + public function __isset($key) + { + return isset($this->getAttributes()[$key]); + } +} diff --git a/src/Support/Definition/GraphQLInterface.php b/src/Support/Definition/GraphQLInterface.php new file mode 100644 index 0000000..db79794 --- /dev/null +++ b/src/Support/Definition/GraphQLInterface.php @@ -0,0 +1,48 @@ +getTypeResolver(); + if(isset($resolver)) + { + $attributes['resolveType'] = $resolver; + } + + return $attributes; + } + + public function toType() + { + return new InterfaceType($this->toArray()); + } + +} diff --git a/src/Support/Definition/GraphQLMutation.php b/src/Support/Definition/GraphQLMutation.php new file mode 100644 index 0000000..65b932b --- /dev/null +++ b/src/Support/Definition/GraphQLMutation.php @@ -0,0 +1,115 @@ +validator = app('validator'); + + $this->graphQL = app('graphql'); + } + + /** + * Get the validation rules. + * + * @return array + */ + public function getRules() + { + $collection = new Collection($this->args()); + + $arguments = func_get_args(); + + return $collection + ->transform(function ($arg) use ($arguments) { + if(isset($arg['rules'])) { + if(is_callable($arg['rules'])) { + return call_user_func_array($arg['rules'], $arguments); + } else { + return $arg['rules']; + } + } + + return null; + }) + ->merge(call_user_func_array([$this, 'rules'], $arguments)) + ->toArray(); + } + + /** + * Get the field resolver. + * + * @return \Closure|null + */ + protected function getResolver() + { + if (!method_exists($this, 'resolve')) { + return null; + } + + $resolver = array($this, 'resolve'); + + return function () use ($resolver) { + $arguments = func_get_args(); + + $this->validate($arguments); + + return call_user_func_array($resolver, $arguments); + }; + } + + /** + * The validation rules for this mutation. + * + * @return array + */ + protected function rules() + { + return []; + } + + /** + * Validate relay mutation. + * + * @param array $args + * @throws ValidationError + * @return void + */ + protected function validate(array $args) + { + $rules = $this->getRules(...$args); + + if (sizeof($rules)) { + $validator = $this->validator->make($args['input'], $rules); + + if ($validator->fails()) { + throw with(new ValidationError('Validation failed', $validator)); + } + } + } +} diff --git a/src/Support/Definition/GraphQLQuery.php b/src/Support/Definition/GraphQLQuery.php new file mode 100644 index 0000000..181eac8 --- /dev/null +++ b/src/Support/Definition/GraphQLQuery.php @@ -0,0 +1,7 @@ +graphQL = app('graphql'); + } + + /** + * Type fields. + * + * @return array + */ + public function fields() + { + return []; + } + + /** + * Get the attributes of the type. + * + * @return array + */ + public function getAttributes() + { + $attributes = array_merge( + $this->attributes, [ + 'fields' => $this->getFields(), + ]); + + if(sizeof($this->interfaces())) { + $attributes['interfaces'] = $this->interfaces(); + } + + return $attributes; + } + + /** + * The resolver for a specific field. + * + * @param $name + * @param $field + * @return \Closure|null + */ + protected function getFieldResolver($name, $field) + { + if(isset($field['resolve'])) { + return $field['resolve']; + } else if(method_exists($this, 'resolve'.studly_case($name).'Field')) { + $resolver = array($this, 'resolve'.studly_case($name).'Field'); + + return function() use ($resolver) { + return call_user_func_array($resolver, func_get_args()); + }; + } + + return null; + } + + /** + * Get the fields of the type. + * + * @return array + */ + public function getFields() + { + $collection = new Collection($this->fields()); + + return $collection->transform(function ($field, $name) { + if(is_string($field)) { + $field = app($field); + + $field->name = $name; + + return $field->toArray(); + } else { + $resolver = $this->getFieldResolver($name, $field); + + if ($resolver) { + $field['resolve'] = $resolver; + } + + return $field; + } + })->toArray(); + } + + /** + * Type interfaces. + * + * @return array + */ + public function interfaces() + { + return []; + } + + /** + * Convert the object to an array. + * + * @return array + */ + public function toArray() + { + return $this->getAttributes(); + } + + /** + * Convert this class to its ObjectType. + * + * @return ObjectType + */ + public function toType() + { + return new ObjectType($this->toArray()); + } + + /** + * Dynamically retrieve the value of an attribute. + * + * @param string $key + * @return mixed + */ + public function __get($key) + { + $attributes = $this->getAttributes(); + + return isset($attributes[$key]) ? $attributes[$key]:null; + } + + /** + * Dynamically check if an attribute is set. + * + * @param string $key + * @return bool + */ + public function __isset($key) + { + return isset($this->getAttributes()[$key]); + } +} diff --git a/src/Types/PageInfoType.php b/src/Support/Definition/PageInfoType.php similarity index 97% rename from src/Types/PageInfoType.php rename to src/Support/Definition/PageInfoType.php index 3460caf..1647512 100644 --- a/src/Types/PageInfoType.php +++ b/src/Support/Definition/PageInfoType.php @@ -1,11 +1,10 @@ validator = $validator; + } + + /** + * Get the messages from the validator. + * + * @return array + */ + public function getValidatorMessages() + { + return $this->validator ? $this->validator->messages() : []; + } +} From c212a71a0205d0eda136c03d608ff77a08213468 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Fri, 4 Mar 2016 13:41:16 -0700 Subject: [PATCH 29/66] Remove remaining Folklore packages --- src/Commands/stubs/field.stub | 2 +- src/Commands/stubs/query.stub | 2 +- src/Node/NodeQuery.php | 2 +- src/Node/NodeType.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Commands/stubs/field.stub b/src/Commands/stubs/field.stub index b441920..d3cce7f 100644 --- a/src/Commands/stubs/field.stub +++ b/src/Commands/stubs/field.stub @@ -4,7 +4,7 @@ namespace DummyNamespace; use GraphQL; use GraphQL\Type\Definition\Type; -use Folklore\GraphQL\Support\Field; +use Nuwave\Relay\Support\Definition\GraphQLField; use Nuwave\Relay\Traits\GlobalIdTrait; class DummyClass extends Field diff --git a/src/Commands/stubs/query.stub b/src/Commands/stubs/query.stub index ae54f0e..7937cad 100644 --- a/src/Commands/stubs/query.stub +++ b/src/Commands/stubs/query.stub @@ -4,7 +4,7 @@ namespace DummyNamespace; use GraphQL; use GraphQL\Type\Definition\Type; -use Folklore\GraphQL\Support\Query; +use Nuwave\Relay\Support\Definition\GraphQLQuery; class DummyClass extends Query { diff --git a/src/Node/NodeQuery.php b/src/Node/NodeQuery.php index c81119d..f865fe4 100644 --- a/src/Node/NodeQuery.php +++ b/src/Node/NodeQuery.php @@ -6,7 +6,7 @@ use Nuwave\Relay\Traits\GlobalIdTrait; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\ResolveInfo; -use Folklore\GraphQL\Support\Query; +use Nuwave\Relay\Support\Definition\GraphQLQuery; class NodeQuery extends Query { diff --git a/src/Node/NodeType.php b/src/Node/NodeType.php index 997dd66..5e73268 100644 --- a/src/Node/NodeType.php +++ b/src/Node/NodeType.php @@ -4,7 +4,7 @@ use GraphQL; use GraphQL\Type\Definition\Type; -use Folklore\GraphQL\Support\InterfaceType; +use Nuwave\Relay\Support\Definition\GraphQLInterface; class NodeType extends InterfaceType { From af84f22c64722cc16e41bb54c4eff0461d771550 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Fri, 4 Mar 2016 13:45:57 -0700 Subject: [PATCH 30/66] remove folklore dependency --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9b68afc..9910a40 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ } }, "require": { - "folklore/graphql": "*", + "webonyx/graphql-php": "~0.5", "illuminate/console": "5.*" } } From 2754a6e7ed5b819663981f3fe9ede173a4e17b69 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Fri, 4 Mar 2016 13:48:05 -0700 Subject: [PATCH 31/66] fix base model name --- src/Node/NodeType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Node/NodeType.php b/src/Node/NodeType.php index 5e73268..9effc1d 100644 --- a/src/Node/NodeType.php +++ b/src/Node/NodeType.php @@ -6,7 +6,7 @@ use GraphQL\Type\Definition\Type; use Nuwave\Relay\Support\Definition\GraphQLInterface; -class NodeType extends InterfaceType +class NodeType extends GraphQLInterface { /** * Interface attributes. From 194828bd26f76ade83af4ce05519969752853ddb Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Fri, 4 Mar 2016 14:39:00 -0700 Subject: [PATCH 32/66] update node type attribute declaration --- src/Commands/SchemaCommand.php | 2 +- src/Node/NodeType.php | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Commands/SchemaCommand.php b/src/Commands/SchemaCommand.php index e7ba6cc..f672f18 100644 --- a/src/Commands/SchemaCommand.php +++ b/src/Commands/SchemaCommand.php @@ -3,7 +3,7 @@ namespace Nuwave\Relay\Commands; use Illuminate\Console\Command; -use Nuwave\Relay\SchemaGenerator; +use Nuwave\Relay\Support\SchemaGenerator; class SchemaCommand extends Command { diff --git a/src/Node/NodeType.php b/src/Node/NodeType.php index 9effc1d..8ae5721 100644 --- a/src/Node/NodeType.php +++ b/src/Node/NodeType.php @@ -13,13 +13,10 @@ class NodeType extends GraphQLInterface * * @var array */ - public function attributes() - { - return [ - 'name' => 'Node', - 'description' => 'An object with an ID.' - ]; - } + protected $attributes = [ + 'name' => 'Node', + 'description' => 'An object with an ID.' + ]; /** * Available fields on type. From 47a0fa9ebb05eb8175ddd8393c73923a924becc9 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Fri, 4 Mar 2016 14:43:14 -0700 Subject: [PATCH 33/66] fix node query import --- src/Node/NodeQuery.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Node/NodeQuery.php b/src/Node/NodeQuery.php index f865fe4..5b8108c 100644 --- a/src/Node/NodeQuery.php +++ b/src/Node/NodeQuery.php @@ -8,7 +8,7 @@ use GraphQL\Type\Definition\ResolveInfo; use Nuwave\Relay\Support\Definition\GraphQLQuery; -class NodeQuery extends Query +class NodeQuery extends GraphQLQuery { use GlobalIdTrait; From 30c7e02af711d49b9effab583ea854f072c09914 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 5 Mar 2016 09:00:03 -0700 Subject: [PATCH 34/66] expose types to avoid circular reference --- src/Schema/GraphQL.php | 25 +++++++++++++++++++++++-- src/Support/Definition/EdgeType.php | 21 ++++++++++++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/Schema/GraphQL.php b/src/Schema/GraphQL.php index 649ce26..330924b 100644 --- a/src/Schema/GraphQL.php +++ b/src/Schema/GraphQL.php @@ -222,6 +222,28 @@ public function type($name, $fresh = false) return $instance; } + /** + * Get if type is registered. + * + * @param string $name + * @return boolean + */ + public function hasType($name) + { + return $this->typeInstances->has($name); + } + + /** + * Get registered type. + * + * @param string $name + * @return \GraphQL\Type\Definition\OutputType + */ + public function getType($name) + { + return $this->typeInstances->get($name); + } + /** * Get instance of connection type. * @@ -301,12 +323,11 @@ protected function checkType($name) */ public function connectionField($name, $resolve = null) { - $edge = $this->type($name); $type = new RelayConnectionType(); $connectionName = (!preg_match('/Connection$/', $name)) ? $name.'Connection' : $name; $type->setName(studly_case($connectionName)); - $type->setEdgeType($edge); + $type->setEdgeType($name); $instance = $type->toType(); $field = [ diff --git a/src/Support/Definition/EdgeType.php b/src/Support/Definition/EdgeType.php index 536b5d7..bdeb994 100644 --- a/src/Support/Definition/EdgeType.php +++ b/src/Support/Definition/EdgeType.php @@ -46,7 +46,13 @@ public function fields() { return [ 'node' => [ - 'type' => $this->type, + 'type' => function () { + if (is_object($this->type)) { + return $this->type; + } + + return $this->getNodeType($this->type); + }, 'description' => 'The item at the end of the edge.', 'resolve' => function ($edge) { return $edge; @@ -91,4 +97,17 @@ public function toType() { return new ObjectType($this->toArray()); } + + /** + * Get node at the end of the edge. + * + * @param string $name + * @return \GraphQL\Type\Definition\OutputType + */ + protected function getNodeType($name) + { + $graphql = app('graphql'); + + return $graphql->hasType($this->type) ? $graphql->getType($this->type) : $graphql->type($this->type); + } } From b10768db6ece9f0b6293f39b9f8f4a399a8c1071 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 5 Mar 2016 15:42:56 -0700 Subject: [PATCH 35/66] add cache to retrieve EloquentType fields --- src/Schema/GraphQL.php | 27 ++++++++ src/Support/Cache/FileStore.php | 61 ++++++++++++++++ src/Support/Definition/EloquentType.php | 92 ++++++++++++++++++------- 3 files changed, 155 insertions(+), 25 deletions(-) create mode 100644 src/Support/Cache/FileStore.php diff --git a/src/Schema/GraphQL.php b/src/Schema/GraphQL.php index 330924b..62d1e1c 100644 --- a/src/Schema/GraphQL.php +++ b/src/Schema/GraphQL.php @@ -62,6 +62,13 @@ class GraphQL */ protected $connectionResolver; + /** + * Instance of cache store. + * + * @var \Nuwave\Relay\Support\Cache\FileStore + */ + protected $cache; + /** * Create a new instance of GraphQL. * @@ -376,4 +383,24 @@ public function getConnectionResolver() { return $this->connectionResolver ?: app(ConnectionResolver::class); } + + /** + * Get instance of cache store. + * + * @return \Nuwave\Relay\Support\Cache\FileStore + */ + public function cache() + { + return $this->cache ?: app(\Nuwave\Relay\Support\Cache\FileStore::class); + } + + /** + * Set instance of Cache store. + * + * @param \Nuwave\Relay\Support\Cache\FileStore + */ + public function setCache(FileStore $cache) + { + $this->cache = $cache; + } } diff --git a/src/Support/Cache/FileStore.php b/src/Support/Cache/FileStore.php new file mode 100644 index 0000000..87e786f --- /dev/null +++ b/src/Support/Cache/FileStore.php @@ -0,0 +1,61 @@ +getPath($name); + + $this->makeDir(dirname($path)); + + return file_put_contents($path, serialize($data)); + } + + /** + * Retrieve data from cache. + * + * @param string $name + * @return mixed|null + */ + public function get($name) + { + if (file_exists($this->getPath($name))) { + return unserialize(file_get_contents($this->getPath($name))); + } + + return null; + } + + /** + * Get path name of item. + * + * @param string $name + * @return string + */ + protected function getPath($name) + { + return storage_path('graphql/cache/'.strtolower($name)); + } + + /** + * Make a directory tree recursively. + * + * @param string $dir + * @return void + */ + public function makeDir($dir) + { + if (! is_dir($dir)) { + mkdir($dir, 0777, true); + } + } +} diff --git a/src/Support/Definition/EloquentType.php b/src/Support/Definition/EloquentType.php index 10df2f4..65b0032 100644 --- a/src/Support/Definition/EloquentType.php +++ b/src/Support/Definition/EloquentType.php @@ -5,6 +5,7 @@ use ReflectionClass; use Illuminate\Database\Eloquent\Model; use GraphQL\Type\Definition\Type; +use GraphQL\Type\Definition\IDType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; @@ -50,18 +51,26 @@ public function __construct(Model $model) */ public function toType() { - if (method_exists($this->model, 'graphqlFields')) { - $this->eloquentFields(); + $graphql = app('graphql'); + $name = $this->getName(); + $description = $this->getDescription(); + + if ($fields = $graphql->cache()->get($name)) { + $this->fields = $fields; } else { + if (method_exists($this->model, 'graphqlFields')) { + $this->eloquentFields(); + } + $this->schemaFields(); + $graphql->cache()->store($name, $this->fields); } - $config = []; - $config['name'] = $this->model->name ?: ucfirst((new ReflectionClass($this->model))->getShortName()); - $config['$description'] = isset($this->model->description) ? $this->model->description : null; - $config['fields'] = $this->fields->toArray(); - - return new ObjectType($config); + return new ObjectType([ + 'name' => $name, + 'description' => $description, + 'fields' => $this->fields->toArray() + ]); } /** @@ -94,19 +103,15 @@ public function eloquentFields() $fields = collect($this->model->graphqlFields()); $fields->each(function ($field, $key) { - if (!$this->skipAutoGenerate($key)) { - $method = 'resolve' . studly_case($key) . 'Field'; - + if (!$this->skipField($key)) { $data = []; $data['type'] = $field['type']; $data['description'] = isset($field['description']) ? $field['description'] : null; if (isset($field['resolve'])) { $data['resolve'] = $field['resolve']; - } elseif (method_exists($this->model, $method)) { - $data['resolve'] = function ($root, $args, $info) use ($method) { - return $this->model->{$method}($root, $args, $info); - }; + } elseif ($method = $this->getModelResolve($key)) { + $data['resolve'] = $method; } $this->fields->put($key, $data); @@ -126,7 +131,7 @@ protected function schemaFields() $columns = collect($schema->getColumnListing($table)); $columns->each(function ($column) use ($table, $schema) { - if (!$this->skipAutoGenerate($column)) { + if (!$this->skipField($column)) { $this->generateField( $column, $schema->getColumnType($table, $column) @@ -152,12 +157,8 @@ protected function generateField($name, $colType) $field['description'] = $field['description'] ?: 'Primary id of type.'; } - $resolve = 'resolve' . studly_case($name); - - if (method_exists($this->model, $resolve)) { - $field['resolve'] = function ($root) use ($resolve) { - return $this->model->{$resolve}($root); - }; + if ($method = $this->getModelResolve($name)) { + $field['resolve'] = $method; } $fieldName = $this->model->camelCase ? camel_case($name) : $name; @@ -177,7 +178,7 @@ protected function resolveTypeByColumn($name, $colType) $type = Type::string(); if ($name === $this->model->getKeyName()) { - $type = Type::nonNull(Type::id()); + $type = Type::id(); } elseif ($colType === 'integer') { $type = Type::int(); } elseif ($colType === 'float' || $colType === 'decimal') { @@ -186,6 +187,10 @@ protected function resolveTypeByColumn($name, $colType) $type = Type::boolean(); } + // Seems a bit odd, but otherwise we'll get an error thrown stating + // that two types have the same name. + $type->name = $this->getName().' '.$type->name; + return $type; } @@ -221,12 +226,49 @@ protected function getRawType(Type $type) * @param string $field * @return boolean */ - protected function skipAutoGenerate($name = '') + protected function skipField($name = '') { - if ($this->hiddenFields->has($name)) { + if ($this->hiddenFields->has($name) || $this->fields->has($name)) { return true; } return false; } + + /** + * Check if model has resolve function. + * + * @param string $key + * @return string|null + */ + protected function getModelResolve($key) + { + $method = 'resolve' . studly_case($key) . 'Field'; + + if (method_exists($this->model, $method)) { + return array($this->model, $method); + } + + return null; + } + + /** + * Get name for type. + * + * @return string + */ + protected function getName() + { + return $this->model->name ?: ucfirst((new ReflectionClass($this->model))->getShortName()); + } + + /** + * Get description of type. + * + * @return string + */ + protected function getDescription() + { + return $this->model->description ?: null; + } } From 87b103bc605e6c3194e63dfc62db9a18ce58719a Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 5 Mar 2016 15:52:38 -0700 Subject: [PATCH 36/66] add cache command --- src/Commands/CacheCommand.php | 35 ++++++++++++++++++++++++++++++++++ src/LaravelServiceProvider.php | 2 ++ src/LumenServiceProvider.php | 2 ++ 3 files changed, 39 insertions(+) create mode 100644 src/Commands/CacheCommand.php diff --git a/src/Commands/CacheCommand.php b/src/Commands/CacheCommand.php new file mode 100644 index 0000000..c5b2f3d --- /dev/null +++ b/src/Commands/CacheCommand.php @@ -0,0 +1,35 @@ +schema(); + + $this->info('Eloquent Types successfully cached.'); + } +} diff --git a/src/LaravelServiceProvider.php b/src/LaravelServiceProvider.php index 095bbd0..e971827 100644 --- a/src/LaravelServiceProvider.php +++ b/src/LaravelServiceProvider.php @@ -8,6 +8,7 @@ use Nuwave\Relay\Commands\QueryMakeCommand; use Nuwave\Relay\Commands\SchemaCommand; use Nuwave\Relay\Commands\TypeMakeCommand; +use Nuwave\Relay\Commands\CacheCommand; use Nuwave\Relay\Schema\Parser; use Nuwave\Relay\Schema\SchemaContainer; use Illuminate\Support\ServiceProvider as BaseProvider; @@ -37,6 +38,7 @@ public function register() { $this->commands([ SchemaCommand::class, + CacheCommand::class, MutationMakeCommand::class, FieldMakeCommand::class, QueryMakeCommand::class, diff --git a/src/LumenServiceProvider.php b/src/LumenServiceProvider.php index eb3a103..447b2e9 100644 --- a/src/LumenServiceProvider.php +++ b/src/LumenServiceProvider.php @@ -8,6 +8,7 @@ use Nuwave\Relay\Commands\QueryMakeCommand; use Nuwave\Relay\Commands\SchemaCommand; use Nuwave\Relay\Commands\TypeMakeCommand; +use Nuwave\Relay\Commands\CacheCommand; use Nuwave\Relay\Schema\Parser; use Nuwave\Relay\Schema\SchemaContainer; use Illuminate\Support\ServiceProvider as BaseProvider; @@ -35,6 +36,7 @@ public function register() { $this->commands([ SchemaCommand::class, + CacheCommand::class, MutationMakeCommand::class, FieldMakeCommand::class, QueryMakeCommand::class, From a0fa9f92a3e9518308eef29b669cba20e9a72795 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sun, 6 Mar 2016 08:21:20 -0700 Subject: [PATCH 37/66] flush cache before generation in command --- src/Commands/CacheCommand.php | 22 ++++++++++++++++++++++ src/Support/Cache/FileStore.php | 14 ++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/Commands/CacheCommand.php b/src/Commands/CacheCommand.php index c5b2f3d..31863c3 100644 --- a/src/Commands/CacheCommand.php +++ b/src/Commands/CacheCommand.php @@ -4,6 +4,7 @@ use Illuminate\Console\Command; use Nuwave\Relay\Support\SchemaGenerator; +use Nuwave\Relay\Support\Cache\FileStore; class CacheCommand extends Command { @@ -21,6 +22,25 @@ class CacheCommand extends Command */ protected $description = 'Cache Eloquent Types.'; + /** + * Cache manager. + * + * @var \Nuwave\Relay\Support\Cache\FileStore + */ + protected $cache; + + /** + * Create new instance of cache command. + * + * @param FileStore $cache + */ + public function __construct(FileStore $cache) + { + parent::__construct(); + + $this->cache = $cache; + } + /** * Execute the console command. * @@ -28,6 +48,8 @@ class CacheCommand extends Command */ public function handle() { + $this->cache->flush(); + app('graphql')->schema(); $this->info('Eloquent Types successfully cached.'); diff --git a/src/Support/Cache/FileStore.php b/src/Support/Cache/FileStore.php index 87e786f..df3cb39 100644 --- a/src/Support/Cache/FileStore.php +++ b/src/Support/Cache/FileStore.php @@ -35,6 +35,20 @@ public function get($name) return null; } + /** + * Remove the cache directory. + * + * @return void + */ + public function flush() + { + $path = $this->getPath(''); + + collect(array_diff(scandir($path), ['..', '.']))->each(function ($file) { + unlink($this->getPath($file)); + }); + } + /** * Get path name of item. * From e72cdb65651f59e713c79ffda3f96efbc9b68b8e Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sun, 6 Mar 2016 08:21:53 -0700 Subject: [PATCH 38/66] allow camel cased fields --- src/Support/Definition/EloquentType.php | 60 +++++++++++++++++-------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/src/Support/Definition/EloquentType.php b/src/Support/Definition/EloquentType.php index 65b0032..0412b61 100644 --- a/src/Support/Definition/EloquentType.php +++ b/src/Support/Definition/EloquentType.php @@ -32,6 +32,13 @@ class EloquentType */ protected $hiddenFields; + /** + * If fields should be camel cased. + * + * @var boolean + */ + protected $camelCase = false; + /** * Create new instance of eloquent type. * @@ -42,6 +49,7 @@ public function __construct(Model $model) $this->fields = collect(); $this->hiddenFields = collect($model->getHidden())->flip(); $this->model = $model; + $this->camelCase = config('relay.camel_case', false); } /** @@ -53,24 +61,26 @@ public function toType() { $graphql = app('graphql'); $name = $this->getName(); - $description = $this->getDescription(); - if ($fields = $graphql->cache()->get($name)) { - $this->fields = $fields; - } else { - if (method_exists($this->model, 'graphqlFields')) { - $this->eloquentFields(); - } + if ($type = $graphql->cache()->get($name)) { + return $type; + } - $this->schemaFields(); - $graphql->cache()->store($name, $this->fields); + if (method_exists($this->model, 'graphqlFields')) { + $this->eloquentFields(); } - return new ObjectType([ + $this->schemaFields(); + + $type = new ObjectType([ 'name' => $name, - 'description' => $description, + 'description' => $this->getDescription(), 'fields' => $this->fields->toArray() ]); + + $graphql->cache()->store($name, $type); + + return $type; } /** @@ -114,7 +124,7 @@ public function eloquentFields() $data['resolve'] = $method; } - $this->fields->put($key, $data); + $this->addField($key, $field); } }); } @@ -161,9 +171,9 @@ protected function generateField($name, $colType) $field['resolve'] = $method; } - $fieldName = $this->model->camelCase ? camel_case($name) : $name; + $fieldName = $this->camelCase ? camel_case($name) : $name; - $this->fields->put($fieldName, $field); + $this->addField($fieldName, $field); } /** @@ -176,21 +186,22 @@ protected function generateField($name, $colType) protected function resolveTypeByColumn($name, $colType) { $type = Type::string(); + $type->name = $this->getName().'_String'; if ($name === $this->model->getKeyName()) { $type = Type::id(); + $type->name = $this->getName().'_ID'; } elseif ($colType === 'integer') { $type = Type::int(); + $type->name = $this->getName().'_Int'; } elseif ($colType === 'float' || $colType === 'decimal') { $type = Type::float(); + $type->name = $this->getName().'_Float'; } elseif ($colType === 'boolean') { $type = Type::boolean(); + $type->name = $this->getName().'_Boolean'; } - // Seems a bit odd, but otherwise we'll get an error thrown stating - // that two types have the same name. - $type->name = $this->getName().' '.$type->name; - return $type; } @@ -220,6 +231,19 @@ protected function getRawType(Type $type) return 'Type::string()'; } + /** + * Add field to collection. + * + * @param string $name + * @param array $field + */ + protected function addField($name, $field) + { + $name = $this->camelCase ? camel_case($name) : $name; + + $this->fields->put($name, $field); + } + /** * Check if field should be skipped. * From 65f9e5755cb8502e104fd00013073b85d67d9117 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sun, 6 Mar 2016 08:28:43 -0700 Subject: [PATCH 39/66] ensure path exists before removing --- src/Support/Cache/FileStore.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Support/Cache/FileStore.php b/src/Support/Cache/FileStore.php index df3cb39..9f8cde9 100644 --- a/src/Support/Cache/FileStore.php +++ b/src/Support/Cache/FileStore.php @@ -44,9 +44,11 @@ public function flush() { $path = $this->getPath(''); - collect(array_diff(scandir($path), ['..', '.']))->each(function ($file) { - unlink($this->getPath($file)); - }); + if (file_exists($path)) { + collect(array_diff(scandir($path), ['..', '.']))->each(function ($file) { + unlink($this->getPath($file)); + }); + } } /** From 64f5af81b9521b1e6c69645289f36fba86b96fd4 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Mon, 7 Mar 2016 11:52:20 -0700 Subject: [PATCH 40/66] grab instances from IoC container --- src/Support/Definition/GraphQLMutation.php | 33 ++-------------------- src/Support/Definition/GraphQLType.php | 18 ------------ 2 files changed, 3 insertions(+), 48 deletions(-) diff --git a/src/Support/Definition/GraphQLMutation.php b/src/Support/Definition/GraphQLMutation.php index 65b932b..dd15975 100644 --- a/src/Support/Definition/GraphQLMutation.php +++ b/src/Support/Definition/GraphQLMutation.php @@ -7,33 +7,6 @@ class GraphQLMutation extends GraphQLField { - /** - * The validator instance. - * - * @var \Illuminate\Validation\Factory - */ - protected $validator; - - /** - * The container instance of GraphQL. - * - * @var \Laravel\Lumen\Application|mixed - */ - protected $graphQL; - - /** - * GraphQLMutation constructor. - * - */ - public function __construct() - { - parent::__construct(); - - $this->validator = app('validator'); - - $this->graphQL = app('graphql'); - } - /** * Get the validation rules. * @@ -47,8 +20,8 @@ public function getRules() return $collection ->transform(function ($arg) use ($arguments) { - if(isset($arg['rules'])) { - if(is_callable($arg['rules'])) { + if (isset($arg['rules'])) { + if (is_callable($arg['rules'])) { return call_user_func_array($arg['rules'], $arguments); } else { return $arg['rules']; @@ -105,7 +78,7 @@ protected function validate(array $args) $rules = $this->getRules(...$args); if (sizeof($rules)) { - $validator = $this->validator->make($args['input'], $rules); + $validator = app('validator')->make($args['input'], $rules); if ($validator->fails()) { throw with(new ValidationError('Validation failed', $validator)); diff --git a/src/Support/Definition/GraphQLType.php b/src/Support/Definition/GraphQLType.php index 8597394..913ab86 100644 --- a/src/Support/Definition/GraphQLType.php +++ b/src/Support/Definition/GraphQLType.php @@ -9,24 +9,6 @@ class GraphQLType extends Fluent { - /** - * The container instance of GraphQL. - * - * @var \Laravel\Lumen\Application|mixed - */ - protected $graphQL; - - /** - * GraphQLType constructor. - * - */ - public function __construct() - { - parent::__construct(); - - $this->graphQL = app('graphql'); - } - /** * Type fields. * From c49e8f169b96665846e9c483b6f2bdf64b8029a9 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Mon, 7 Mar 2016 12:03:48 -0700 Subject: [PATCH 41/66] only cache generated fields --- src/Support/Definition/EloquentType.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Support/Definition/EloquentType.php b/src/Support/Definition/EloquentType.php index 0412b61..2a8c47e 100644 --- a/src/Support/Definition/EloquentType.php +++ b/src/Support/Definition/EloquentType.php @@ -62,24 +62,23 @@ public function toType() $graphql = app('graphql'); $name = $this->getName(); - if ($type = $graphql->cache()->get($name)) { - return $type; + if ($fields = $graphql->cache()->get($name)) { + $this->fields = $fields; + } else { + $this->schemaFields(); + $graphql->cache()->store($name, $this->fields); } if (method_exists($this->model, 'graphqlFields')) { $this->eloquentFields(); } - $this->schemaFields(); - $type = new ObjectType([ 'name' => $name, 'description' => $this->getDescription(), 'fields' => $this->fields->toArray() ]); - $graphql->cache()->store($name, $type); - return $type; } From 563c81b4bba769e449e2b16bd0c33d97d7c64713 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Mon, 7 Mar 2016 12:28:47 -0700 Subject: [PATCH 42/66] store edge instances --- src/Schema/GraphQL.php | 38 +++++++++++++++++++++++++ src/Support/Definition/EloquentType.php | 4 +-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/Schema/GraphQL.php b/src/Schema/GraphQL.php index 62d1e1c..376701e 100644 --- a/src/Schema/GraphQL.php +++ b/src/Schema/GraphQL.php @@ -55,6 +55,13 @@ class GraphQL */ protected $connectionInstances; + /** + * Collection of connection edge instances. + * + * @var \Illuminate\Support\Collection + */ + protected $edgeInstances; + /** * Instance of connection resolver. * @@ -83,6 +90,7 @@ public function __construct($app) $this->mutations = collect(); $this->typeInstances = collect(); $this->connectionInstances = collect(); + $this->edgeInstances = collect(); } /** @@ -283,6 +291,19 @@ public function connection($name, $resolve = null, $fresh = false) return $field; } + /** + * Get instance of edge type. + * + * @param string $name + * @return \GraphQL\Type\Definition\ObjectType|null + */ + public function edge($name) + { + $this->checkType($name); + + return $this->edgeInstances->get($name); + } + /** * Format error for output. * @@ -336,6 +357,7 @@ public function connectionField($name, $resolve = null) $type->setName(studly_case($connectionName)); $type->setEdgeType($name); $instance = $type->toType(); + $this->addEdge($instance, $name); $field = [ 'args' => RelayConnectionType::connectionArgs(), @@ -350,6 +372,22 @@ public function connectionField($name, $resolve = null) return $field; } + /** + * Add edge instance. + * + * @param ObjectType $type + * @param string $name + * @return void + */ + public function addEdge(ObjectType $type, $name) + { + if ($edges = $type->getField('edges')) { + $type = $edges->getType()->getWrappedType(); + + $this->edgeInstances->put($name, $type); + } + } + /** * Auto-resolve connection. * diff --git a/src/Support/Definition/EloquentType.php b/src/Support/Definition/EloquentType.php index 2a8c47e..82e5846 100644 --- a/src/Support/Definition/EloquentType.php +++ b/src/Support/Definition/EloquentType.php @@ -73,13 +73,11 @@ public function toType() $this->eloquentFields(); } - $type = new ObjectType([ + return new ObjectType([ 'name' => $name, 'description' => $this->getDescription(), 'fields' => $this->fields->toArray() ]); - - return $type; } /** From 21f8500457cfa985204445417c9bd29c2902e91c Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Mon, 7 Mar 2016 14:48:07 -0700 Subject: [PATCH 43/66] remove graphql config settings --- config/config.php | 8 +++++++- src/LaravelServiceProvider.php | 16 ++++++++-------- src/LumenServiceProvider.php | 16 ++++++++-------- src/Node/NodeQuery.php | 2 +- src/Schema/GraphQL.php | 6 +++--- src/Support/SchemaGenerator.php | 2 +- src/Traits/MutationTestTrait.php | 2 +- 7 files changed, 29 insertions(+), 23 deletions(-) diff --git a/config/config.php b/config/config.php index 40b2e0b..a1c9a7f 100644 --- a/config/config.php +++ b/config/config.php @@ -32,7 +32,13 @@ | */ - 'schema_path' => 'Http/GraphQL/schema.php', + 'schema' => [ + 'file' => 'Http/GraphQL/schema.php', + 'output' => null, + 'types' => [], + 'mutations' => [], + 'queries' => [] + ], 'model_path' => 'App\\Models', 'camel_case' => false, diff --git a/src/LaravelServiceProvider.php b/src/LaravelServiceProvider.php index e971827..ab97a7a 100644 --- a/src/LaravelServiceProvider.php +++ b/src/LaravelServiceProvider.php @@ -61,7 +61,7 @@ public function register() */ protected function registerSchema() { - require_once app_path(config('relay.schema_path')); + require_once app_path(config('relay.schema.path')); $this->setGraphQLConfig(); @@ -95,14 +95,14 @@ protected function setGraphQLConfig() { $relay = $this->app['relay']; - $mutations = config('graphql.schema.mutation', []); - $queries = config('graphql.schema.query', []); - $types = config('graphql.types', []); + $mutations = config('relay.schema.mutations', []); + $queries = config('relay.schema.queries', []); + $types = config('relay.schema.types', []); config([ - 'graphql.schema.mutation' => array_merge($mutations, $relay->getMutations()->config()), - 'graphql.schema.query' => array_merge($queries, $relay->getQueries()->config()), - 'graphql.types' => array_merge($types, $relay->getTypes()->config()) + 'relay.schema.mutations' => array_merge($mutations, $relay->getMutations()->config()), + 'relay.schema.queries' => array_merge($queries, $relay->getQueries()->config()), + 'relay.schema.types' => array_merge($types, $relay->getTypes()->config()) ]); } @@ -113,7 +113,7 @@ protected function setGraphQLConfig() */ protected function initializeTypes() { - foreach (config('graphql.types') as $name => $type) { + foreach (config('relay.schema.types') as $name => $type) { $this->app['graphql']->addType($type, $name); } } diff --git a/src/LumenServiceProvider.php b/src/LumenServiceProvider.php index 447b2e9..0d0a036 100644 --- a/src/LumenServiceProvider.php +++ b/src/LumenServiceProvider.php @@ -61,7 +61,7 @@ protected function registerSchema() { $this->registerRelayTypes(); - require_once __DIR__ . '/../../../../app/' . config('relay.schema_path'); + require_once __DIR__ . '/../../../../app/' . config('relay.schema.path'); $this->setGraphQLConfig(); @@ -93,14 +93,14 @@ protected function setGraphQLConfig() { $relay = $this->app['relay']; - $mutations = config('graphql.schema.mutation', []); - $queries = config('graphql.schema.query', []); - $types = config('graphql.types', []); + $mutations = config('relay.schema.mutations', []); + $queries = config('relay.schema.queries', []); + $types = config('relay.schema.types', []); config([ - 'graphql.schema.mutation' => array_merge($mutations, $relay->getMutations()->config()), - 'graphql.schema.query' => array_merge($queries, $relay->getQueries()->config()), - 'graphql.types' => array_merge($types, $relay->getTypes()->config()) + 'relay.schema.mutations' => array_merge($mutations, $relay->getMutations()->config()), + 'relay.schema.queries' => array_merge($queries, $relay->getQueries()->config()), + 'relay.schema.types' => array_merge($types, $relay->getTypes()->config()) ]); } @@ -111,7 +111,7 @@ protected function setGraphQLConfig() */ protected function initializeTypes() { - foreach (config('graphql.types') as $name => $type) { + foreach (config('relay.schema.types') as $name => $type) { $this->app['graphql']->addType($type, $name); } } diff --git a/src/Node/NodeQuery.php b/src/Node/NodeQuery.php index 5b8108c..e1d2ee3 100644 --- a/src/Node/NodeQuery.php +++ b/src/Node/NodeQuery.php @@ -50,7 +50,7 @@ public function resolve($root, array $args, ResolveInfo $info) // as well as the type's name. list($typeClass, $id) = $this->decodeGlobalId($args['id']); - foreach (config('graphql.types') as $type => $class) { + foreach (config('relay.schema.types') as $type => $class) { if ($typeClass == $class) { $objectType = app($typeClass); diff --git a/src/Schema/GraphQL.php b/src/Schema/GraphQL.php index 376701e..6c59e33 100644 --- a/src/Schema/GraphQL.php +++ b/src/Schema/GraphQL.php @@ -133,14 +133,14 @@ public function queryAndReturnResult($query, $variables = []) */ public function schema() { - $schema = config('graphql.schema'); + $schema = config('relay.schema'); $this->types->each(function ($type, $key) { $this->type($key); }); - $queries = $this->queries->merge(array_get($schema, 'query', [])); - $mutations = $this->mutations->merge(array_get($schema, 'mutation', [])); + $queries = $this->queries->merge(array_get($schema, 'queries', [])); + $mutations = $this->mutations->merge(array_get($schema, 'mutations', [])); $queryTypes = $this->generateType($queries, ['name' => 'Query']); $mutationTypes = $this->generateType($mutations, ['name' => 'Mutation']); diff --git a/src/Support/SchemaGenerator.php b/src/Support/SchemaGenerator.php index 3802ee0..1b93a29 100644 --- a/src/Support/SchemaGenerator.php +++ b/src/Support/SchemaGenerator.php @@ -19,7 +19,7 @@ public function execute($version = '4.12') if (isset($data['data']['__schema'])) { $schema = json_encode($data); - $path = config('graphql.schema_path') ?: storage_path('relay/schema.json'); + $path = config('relay.schema.output') ?: storage_path('relay/schema.json'); $this->put($path, $schema); } diff --git a/src/Traits/MutationTestTrait.php b/src/Traits/MutationTestTrait.php index ed61223..f60b9eb 100644 --- a/src/Traits/MutationTestTrait.php +++ b/src/Traits/MutationTestTrait.php @@ -76,7 +76,7 @@ protected function generateOutputFields($mutationName, array $outputFields) protected function availableOutputFields($mutationName) { $outputFields = ['clientMutationId']; - $mutations = config('graphql.schema.mutation'); + $mutations = config('relay.schema.mutations'); $mutation = app($mutations[$mutationName]); foreach ($mutation->type()->getFields() as $name => $field) { From 32f072444513016c486c3a1842147e471aeb6cbf Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Mon, 7 Mar 2016 14:53:56 -0700 Subject: [PATCH 44/66] update tests --- tests/BaseTest.php | 32 +++++++------------- tests/assets/Queries/UpdateHeroNameQuery.php | 4 +-- tests/assets/Types/HumanType.php | 2 +- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/tests/BaseTest.php b/tests/BaseTest.php index c2f9983..a539964 100644 --- a/tests/BaseTest.php +++ b/tests/BaseTest.php @@ -32,7 +32,6 @@ protected function graphqlResponse($query, $variables = [], $encode = false) protected function getPackageProviders($app) { return [ - \Folklore\GraphQL\GraphQLServiceProvider::class, \Nuwave\Relay\LaravelServiceProvider::class, ]; } @@ -46,8 +45,8 @@ protected function getPackageProviders($app) protected function getPackageAliases($app) { return [ - 'GraphQL' => \Folklore\GraphQL\Support\Facades\GraphQL::class, - 'Relay' => \Nuwave\Relay\LaravelServiceProvider::class, + 'GraphQL' => \Nuwave\Relay\Facades\GraphQL::class, + 'Relay' => \Nuwave\Relay\Facades\Relay::class, ]; } @@ -59,29 +58,20 @@ protected function getPackageAliases($app) */ protected function getEnvironmentSetUp($app) { - $app['config']->set('graphql', [ - 'prefix' => 'graphql', - 'routes' => '/', - 'controllers' => '\Folklore\GraphQL\GraphQLController@query', - 'middleware' => [], + $app['config']->set('relay', [ 'schema' => [ - 'query' => [ + 'path' => 'schema/schema.php', + 'queries' => [ 'node' => \Nuwave\Relay\Node\NodeQuery::class, 'humanByName' => \Nuwave\Relay\Tests\Assets\Queries\HumanByName::class, ], - 'mutation' => [ - - ] - ], - 'types' => [ - 'node' => \Nuwave\Relay\Node\NodeType::class, - 'pageInfo' => \Nuwave\Relay\Types\PageInfoType::class, - 'human' => \Nuwave\Relay\Tests\Assets\Types\HumanType::class, + 'mutations' => [], + 'types' => [ + 'node' => \Nuwave\Relay\Node\NodeType::class, + 'pageInfo' => \Nuwave\Relay\Support\Definition\PageInfoType::class, + 'human' => \Nuwave\Relay\Tests\Assets\Types\HumanType::class, + ], ] ]); - - $app['config']->set('relay', [ - 'schema_path' => 'schema/schema.php' - ]); } } diff --git a/tests/assets/Queries/UpdateHeroNameQuery.php b/tests/assets/Queries/UpdateHeroNameQuery.php index 984b387..e3cd3ee 100644 --- a/tests/assets/Queries/UpdateHeroNameQuery.php +++ b/tests/assets/Queries/UpdateHeroNameQuery.php @@ -3,9 +3,9 @@ namespace Nuwave\Relay\Tests\Assets; use GraphQL\Type\Definition\ResolveInfo; -use Nuwave\Relay\Mutations\MutationWithClientId; +use Nuwave\Relay\Support\Definition\RelayMutation; -class UpdateHeroNameQuery extends MutationWithClientId +class UpdateHeroNameQuery extends RelayMutation { /** * Name of Mutation. diff --git a/tests/assets/Types/HumanType.php b/tests/assets/Types/HumanType.php index 2790cb1..3c86781 100644 --- a/tests/assets/Types/HumanType.php +++ b/tests/assets/Types/HumanType.php @@ -2,7 +2,7 @@ namespace Nuwave\Relay\Tests\Assets\Types; -use Nuwave\Relay\Types\RelayType; +use Nuwave\Relay\Support\Definition\RelayType; use Nuwave\Relay\Tests\Assets\Data\StarWarsData; use GraphQL\Type\Definition\Type; From 07d46987e2f8f4c6be9b192226b6bc5861aec1a1 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Tue, 8 Mar 2016 10:47:30 -0700 Subject: [PATCH 45/66] fields for eloquent models attached to mutliple types --- src/Schema/GraphQL.php | 2 +- src/Support/Definition/EloquentType.php | 48 ++++++++++++++++++++----- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/Schema/GraphQL.php b/src/Schema/GraphQL.php index 6c59e33..ebf913c 100644 --- a/src/Schema/GraphQL.php +++ b/src/Schema/GraphQL.php @@ -226,7 +226,7 @@ public function type($name, $fresh = false) $type = app($type); } - $instance = $type instanceof Model ? (new EloquentType($type))->toType() : $type->toType(); + $instance = $type instanceof Model ? (new EloquentType($type, $name))->toType() : $type->toType(); $this->typeInstances->put($name, $instance); diff --git a/src/Support/Definition/EloquentType.php b/src/Support/Definition/EloquentType.php index 82e5846..bd17215 100644 --- a/src/Support/Definition/EloquentType.php +++ b/src/Support/Definition/EloquentType.php @@ -3,6 +3,7 @@ namespace Nuwave\Relay\Support\Definition; use ReflectionClass; +use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Model; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\IDType; @@ -18,6 +19,13 @@ class EloquentType */ protected $model; + /** + * Registered type name. + * + * @var string + */ + protected $name; + /** * Available fields. * @@ -44,8 +52,9 @@ class EloquentType * * @param Model $model */ - public function __construct(Model $model) + public function __construct(Model $model, $name = '') { + $this->name = $name; $this->fields = collect(); $this->hiddenFields = collect($model->getHidden())->flip(); $this->model = $model; @@ -70,7 +79,12 @@ public function toType() } if (method_exists($this->model, 'graphqlFields')) { - $this->eloquentFields(); + $this->eloquentFields(collect($this->model->graphqlFields())); + } + + if (method_exists($this->model, $this->getTypeMethod())) { + $method = $this->getTypeMethod(); + $this->eloquentFields(collect($this->model->{$method}())); } return new ObjectType([ @@ -87,10 +101,15 @@ public function toType() */ public function rawFields() { + $this->schemaFields(); + if (method_exists($this->model, 'graphqlFields')) { - $this->eloquentFields(); - } else { - $this->schemaFields(); + $this->eloquentFields(collect($this->model->graphqlFields())); + } + + if (method_exists($this->model, $this->getTypeMethod())) { + $method = $this->getTypeMethod(); + $this->eloquentFields(collect($this->model->{$method}())); } return $this->fields->transform(function ($field, $key) { @@ -103,12 +122,11 @@ public function rawFields() /** * Convert eloquent defined fields. * + * @param \Illuminate\Support\Collection * @return array */ - public function eloquentFields() + public function eloquentFields(Collection $fields) { - $fields = collect($this->model->graphqlFields()); - $fields->each(function ($field, $key) { if (!$this->skipField($key)) { $data = []; @@ -280,6 +298,10 @@ protected function getModelResolve($key) */ protected function getName() { + if ($this->name) { + return studly_case($this->name); + } + return $this->model->name ?: ucfirst((new ReflectionClass($this->model))->getShortName()); } @@ -292,4 +314,14 @@ protected function getDescription() { return $this->model->description ?: null; } + + /** + * Get method name for type. + * + * @return string + */ + protected function getTypeMethod() + { + return 'graphql'.$this->getName().'Fields'; + } } From df4b69e0383acc63ccd4cb55419fdca993af1acb Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Wed, 9 Mar 2016 12:46:26 -0700 Subject: [PATCH 46/66] update configuration --- config/config.php | 2 +- src/Commands/TypeMakeCommand.php | 2 +- src/LaravelServiceProvider.php | 4 +++- src/LumenServiceProvider.php | 4 +++- src/Support/ConnectionResolver.php | 2 +- src/Support/Definition/EloquentType.php | 2 +- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/config/config.php b/config/config.php index a1c9a7f..34d5f1a 100644 --- a/config/config.php +++ b/config/config.php @@ -33,7 +33,7 @@ */ 'schema' => [ - 'file' => 'Http/GraphQL/schema.php', + 'file' => null, 'output' => null, 'types' => [], 'mutations' => [], diff --git a/src/Commands/TypeMakeCommand.php b/src/Commands/TypeMakeCommand.php index 8564bd9..0ed846f 100644 --- a/src/Commands/TypeMakeCommand.php +++ b/src/Commands/TypeMakeCommand.php @@ -108,7 +108,7 @@ protected function getEloquentStub($model) if (starts_with($model, $rootNamespace)) { $shortName = (new ReflectionClass($model))->getShortName(); } else { - $model = config('relay.model_path') . "\\" . $model; + $model = config('relay.eloquent.path') . "\\" . $model; } $fields = $this->getTypeFields($model); diff --git a/src/LaravelServiceProvider.php b/src/LaravelServiceProvider.php index ab97a7a..a91f6a2 100644 --- a/src/LaravelServiceProvider.php +++ b/src/LaravelServiceProvider.php @@ -61,7 +61,9 @@ public function register() */ protected function registerSchema() { - require_once app_path(config('relay.schema.path')); + if (config('relay.schema.path')) { + require_once app_path(config('relay.schema.path')); + } $this->setGraphQLConfig(); diff --git a/src/LumenServiceProvider.php b/src/LumenServiceProvider.php index 0d0a036..edcbd01 100644 --- a/src/LumenServiceProvider.php +++ b/src/LumenServiceProvider.php @@ -61,7 +61,9 @@ protected function registerSchema() { $this->registerRelayTypes(); - require_once __DIR__ . '/../../../../app/' . config('relay.schema.path'); + if (config('relay.schema.path')) { + require_once __DIR__ . '/../../../../app/' . config('relay.schema.path'); + } $this->setGraphQLConfig(); diff --git a/src/Support/ConnectionResolver.php b/src/Support/ConnectionResolver.php index da4a941..28df7e7 100644 --- a/src/Support/ConnectionResolver.php +++ b/src/Support/ConnectionResolver.php @@ -79,7 +79,7 @@ protected function getItems($collection, ResolveInfo $info, $name) */ protected function getSelectFields(ResolveInfo $info) { - $camel = config('relay.camel_case'); + $camel = config('relay.eloquent.camel_case'); return collect($info->getFieldSelection(4)['edges']['node']) ->reject(function ($value) { diff --git a/src/Support/Definition/EloquentType.php b/src/Support/Definition/EloquentType.php index bd17215..e41d3e5 100644 --- a/src/Support/Definition/EloquentType.php +++ b/src/Support/Definition/EloquentType.php @@ -58,7 +58,7 @@ public function __construct(Model $model, $name = '') $this->fields = collect(); $this->hiddenFields = collect($model->getHidden())->flip(); $this->model = $model; - $this->camelCase = config('relay.camel_case', false); + $this->camelCase = config('relay.eloquent.camel_case', false); } /** From 8369c727fb69eded75f7c331ea62ec4939094902 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Wed, 9 Mar 2016 12:51:50 -0700 Subject: [PATCH 47/66] fix namespace --- src/Commands/TypeMakeCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Commands/TypeMakeCommand.php b/src/Commands/TypeMakeCommand.php index 0ed846f..582ab1e 100644 --- a/src/Commands/TypeMakeCommand.php +++ b/src/Commands/TypeMakeCommand.php @@ -3,7 +3,7 @@ namespace Nuwave\Relay\Commands; use ReflectionClass; -use Nuwave\Relay\Types\EloquentType; +use Nuwave\Relay\Support\Definition\EloquentType; use Illuminate\Console\GeneratorCommand; use Symfony\Component\Console\Input\InputOption; @@ -108,7 +108,7 @@ protected function getEloquentStub($model) if (starts_with($model, $rootNamespace)) { $shortName = (new ReflectionClass($model))->getShortName(); } else { - $model = config('relay.eloquent.path') . "\\" . $model; + $model = config('relay.model_path') . "\\" . $model; } $fields = $this->getTypeFields($model); From e7811fbe3b4f2cbd7022be83af6d07564e9ca953 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Wed, 9 Mar 2016 12:55:53 -0700 Subject: [PATCH 48/66] add doctrine to requirements --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9910a40..a043d17 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ }, "require": { "webonyx/graphql-php": "~0.5", - "illuminate/console": "5.*" + "illuminate/console": "5.*", + "doctrine/dbal": "^2.5" } } From d1069c2a8d93a0e26a8e40f951c4f8759a8a36e5 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Wed, 9 Mar 2016 13:04:46 -0700 Subject: [PATCH 49/66] fix extended class names --- src/Commands/stubs/field.stub | 2 +- src/Commands/stubs/query.stub | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Commands/stubs/field.stub b/src/Commands/stubs/field.stub index d3cce7f..2707abf 100644 --- a/src/Commands/stubs/field.stub +++ b/src/Commands/stubs/field.stub @@ -7,7 +7,7 @@ use GraphQL\Type\Definition\Type; use Nuwave\Relay\Support\Definition\GraphQLField; use Nuwave\Relay\Traits\GlobalIdTrait; -class DummyClass extends Field +class DummyClass extends GraphQLField { /** * Field attributes. diff --git a/src/Commands/stubs/query.stub b/src/Commands/stubs/query.stub index 7937cad..b2e494f 100644 --- a/src/Commands/stubs/query.stub +++ b/src/Commands/stubs/query.stub @@ -6,7 +6,7 @@ use GraphQL; use GraphQL\Type\Definition\Type; use Nuwave\Relay\Support\Definition\GraphQLQuery; -class DummyClass extends Query +class DummyClass extends GraphQLQuery { /** * Type query returns. From e2410926b7d5b55e6e509aff674b343da0d62c96 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Wed, 9 Mar 2016 13:26:09 -0700 Subject: [PATCH 50/66] rename file to path --- config/config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.php b/config/config.php index 34d5f1a..3c4a970 100644 --- a/config/config.php +++ b/config/config.php @@ -33,7 +33,7 @@ */ 'schema' => [ - 'file' => null, + 'path' => null, 'output' => null, 'types' => [], 'mutations' => [], From d81b74d3d9abd9b13c54b88abf4546f97e743125 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Wed, 9 Mar 2016 13:26:31 -0700 Subject: [PATCH 51/66] register relay types before populating config --- src/LaravelServiceProvider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LaravelServiceProvider.php b/src/LaravelServiceProvider.php index a91f6a2..03e7024 100644 --- a/src/LaravelServiceProvider.php +++ b/src/LaravelServiceProvider.php @@ -65,10 +65,10 @@ protected function registerSchema() require_once app_path(config('relay.schema.path')); } - $this->setGraphQLConfig(); - $this->registerRelayTypes(); + $this->setGraphQLConfig(); + $this->initializeTypes(); } From ee34e0d56a03bab7f056fe62aff982dfc89a556b Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Wed, 9 Mar 2016 14:10:28 -0700 Subject: [PATCH 52/66] fix issue with validation --- src/Schema/GraphQL.php | 2 +- src/Support/Definition/GraphQLMutation.php | 73 +++++++++------------- src/Support/ValidationError.php | 14 ++--- 3 files changed, 37 insertions(+), 52 deletions(-) diff --git a/src/Schema/GraphQL.php b/src/Schema/GraphQL.php index ebf913c..396724d 100644 --- a/src/Schema/GraphQL.php +++ b/src/Schema/GraphQL.php @@ -323,7 +323,7 @@ public function formatError(Error $e) $previous = $e->getPrevious(); if ($previous && $previous instanceof ValidationError) { - $error['validation'] = $previous->getValidatorMessage(); + $error['validation'] = $previous->getValidatorMessages(); } return $error; diff --git a/src/Support/Definition/GraphQLMutation.php b/src/Support/Definition/GraphQLMutation.php index dd15975..fe6b4a9 100644 --- a/src/Support/Definition/GraphQLMutation.php +++ b/src/Support/Definition/GraphQLMutation.php @@ -3,39 +3,48 @@ namespace Nuwave\Relay\Support\Definition; use Illuminate\Support\Collection; +use Nuwave\Relay\Support\ValidationError; use Nuwave\Relay\Schema\GraphQL; +use Nuwave\Relay\Traits\GlobalIdTrait; class GraphQLMutation extends GraphQLField { + use GlobalIdTrait; + /** - * Get the validation rules. + * Rules to apply to mutation. * * @return array */ - public function getRules() + protected function rules() { - $collection = new Collection($this->args()); + return []; + } + /** + * Get rules for mutation. + * + * @return array + */ + public function getRules() + { $arguments = func_get_args(); - return $collection - ->transform(function ($arg) use ($arguments) { + return collect($this->args()) + ->transform(function ($arg, $name) use ($arguments) { if (isset($arg['rules'])) { - if (is_callable($arg['rules'])) { + if (is_callable($args['rules'])) { return call_user_func_array($arg['rules'], $arguments); - } else { - return $arg['rules']; } + return $arg['rules']; } - return null; - }) - ->merge(call_user_func_array([$this, 'rules'], $arguments)) + })->merge(call_user_func_array([$this, 'rules'], $arguments)) ->toArray(); } /** - * Get the field resolver. + * Get the mutation resolver. * * @return \Closure|null */ @@ -49,40 +58,18 @@ protected function getResolver() return function () use ($resolver) { $arguments = func_get_args(); + $rules = call_user_func_array([$this, 'getRules'], $arguments); + + if (sizeof($rules)) { + $args = array_get($arguments, 1, []); + $validator = app('validator')->make($args, $rules); - $this->validate($arguments); + if ($validator->fails()) { + throw with(new ValidationError('validation'))->setValidator($validator); + } + } return call_user_func_array($resolver, $arguments); }; } - - /** - * The validation rules for this mutation. - * - * @return array - */ - protected function rules() - { - return []; - } - - /** - * Validate relay mutation. - * - * @param array $args - * @throws ValidationError - * @return void - */ - protected function validate(array $args) - { - $rules = $this->getRules(...$args); - - if (sizeof($rules)) { - $validator = app('validator')->make($args['input'], $rules); - - if ($validator->fails()) { - throw with(new ValidationError('Validation failed', $validator)); - } - } - } } diff --git a/src/Support/ValidationError.php b/src/Support/ValidationError.php index cfed8c1..c7ada61 100644 --- a/src/Support/ValidationError.php +++ b/src/Support/ValidationError.php @@ -3,28 +3,26 @@ namespace Nuwave\Relay\Support; use GraphQL\Error; -use Illuminate\Validation\Validator; class ValidationError extends Error { /** * The validator. * - * @var Validator + * @var \Illuminate\Validation\Validator */ protected $validator; /** - * ValidationError constructor. + * Set validator instance. * - * @param \Exception|string $message - * @param Validator $validator + * @param mixed $validator */ - public function __construct($message, Validator $validator) + public function setValidator($validator) { - parent::__construct($message); - $this->validator = $validator; + + return $this; } /** From ff8f5f11b1c84ccf3bd99c842e4a2ff87fefbcc4 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Wed, 9 Mar 2016 14:32:46 -0700 Subject: [PATCH 53/66] move validation to field mutations, queries and fields now have validation --- src/Support/Definition/GraphQLField.php | 62 ++++++++++++++++++-- src/Support/Definition/GraphQLMutation.php | 67 ---------------------- src/Support/Definition/GraphQLQuery.php | 3 +- 3 files changed, 59 insertions(+), 73 deletions(-) diff --git a/src/Support/Definition/GraphQLField.php b/src/Support/Definition/GraphQLField.php index 940659d..a3896ff 100644 --- a/src/Support/Definition/GraphQLField.php +++ b/src/Support/Definition/GraphQLField.php @@ -3,11 +3,15 @@ namespace Nuwave\Relay\Support\Definition; use Illuminate\Support\Fluent; -use Nuwave\Relay\Schema\GraphQL; use Illuminate\Support\DefinitionsFluent; +use Nuwave\Relay\Schema\GraphQL; +use Nuwave\Relay\Traits\GlobalIdTrait; +use Nuwave\Relay\Support\ValidationError; class GraphQLField extends Fluent { + use GlobalIdTrait; + /** * The container instance of GraphQL. * @@ -56,6 +60,16 @@ public function type() return null; } + /** + * Rules to apply to mutation. + * + * @return array + */ + public function rules() + { + return []; + } + /** * Get the attributes of the field. * @@ -75,20 +89,58 @@ public function getAttributes() } /** - * Get the field resolver. + * Get rules for mutation. + * + * @return array + */ + public function getRules() + { + $arguments = func_get_args(); + + return collect($this->args()) + ->transform(function ($arg, $name) use ($arguments) { + if (isset($arg['rules'])) { + if (is_callable($arg['rules'])) { + return call_user_func_array($arg['rules'], $arguments); + } + return $arg['rules']; + } + return null; + }) + ->merge(call_user_func_array([$this, 'rules'], $arguments)) + ->reject(function ($arg) { + return is_null($arg); + }) + ->toArray(); + } + + /** + * Get the mutation resolver. * * @return \Closure|null */ protected function getResolver() { - if(!method_exists($this, 'resolve')) { + if (!method_exists($this, 'resolve')) { return null; } $resolver = array($this, 'resolve'); - return function() use ($resolver) { - return call_user_func_array($resolver, func_get_args()); + return function () use ($resolver) { + $arguments = func_get_args(); + $rules = call_user_func_array([$this, 'getRules'], $arguments); + + if (sizeof($rules)) { + $args = array_get($arguments, 1, []); + $validator = app('validator')->make($args, $rules); + + if ($validator->fails()) { + throw with(new ValidationError('validation'))->setValidator($validator); + } + } + + return call_user_func_array($resolver, $arguments); }; } diff --git a/src/Support/Definition/GraphQLMutation.php b/src/Support/Definition/GraphQLMutation.php index fe6b4a9..7c9e138 100644 --- a/src/Support/Definition/GraphQLMutation.php +++ b/src/Support/Definition/GraphQLMutation.php @@ -2,74 +2,7 @@ namespace Nuwave\Relay\Support\Definition; -use Illuminate\Support\Collection; -use Nuwave\Relay\Support\ValidationError; -use Nuwave\Relay\Schema\GraphQL; -use Nuwave\Relay\Traits\GlobalIdTrait; - class GraphQLMutation extends GraphQLField { - use GlobalIdTrait; - - /** - * Rules to apply to mutation. - * - * @return array - */ - protected function rules() - { - return []; - } - - /** - * Get rules for mutation. - * - * @return array - */ - public function getRules() - { - $arguments = func_get_args(); - - return collect($this->args()) - ->transform(function ($arg, $name) use ($arguments) { - if (isset($arg['rules'])) { - if (is_callable($args['rules'])) { - return call_user_func_array($arg['rules'], $arguments); - } - return $arg['rules']; - } - return null; - })->merge(call_user_func_array([$this, 'rules'], $arguments)) - ->toArray(); - } - - /** - * Get the mutation resolver. - * - * @return \Closure|null - */ - protected function getResolver() - { - if (!method_exists($this, 'resolve')) { - return null; - } - - $resolver = array($this, 'resolve'); - - return function () use ($resolver) { - $arguments = func_get_args(); - $rules = call_user_func_array([$this, 'getRules'], $arguments); - - if (sizeof($rules)) { - $args = array_get($arguments, 1, []); - $validator = app('validator')->make($args, $rules); - - if ($validator->fails()) { - throw with(new ValidationError('validation'))->setValidator($validator); - } - } - return call_user_func_array($resolver, $arguments); - }; - } } diff --git a/src/Support/Definition/GraphQLQuery.php b/src/Support/Definition/GraphQLQuery.php index 181eac8..c7c2c10 100644 --- a/src/Support/Definition/GraphQLQuery.php +++ b/src/Support/Definition/GraphQLQuery.php @@ -2,6 +2,7 @@ namespace Nuwave\Relay\Support\Definition; -class GraphQLQuery extends GraphQLField { +class GraphQLQuery extends GraphQLField +{ } From 4153827087f2d1213f31415aa4eacc49b622a8bb Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Wed, 9 Mar 2016 16:23:19 -0700 Subject: [PATCH 54/66] use same validation method for relay mutation --- src/Support/Definition/GraphQLField.php | 46 +++++++------- src/Support/Definition/RelayMutation.php | 80 +++++------------------- 2 files changed, 39 insertions(+), 87 deletions(-) diff --git a/src/Support/Definition/GraphQLField.php b/src/Support/Definition/GraphQLField.php index a3896ff..7e69bd8 100644 --- a/src/Support/Definition/GraphQLField.php +++ b/src/Support/Definition/GraphQLField.php @@ -12,24 +12,6 @@ class GraphQLField extends Fluent { use GlobalIdTrait; - /** - * The container instance of GraphQL. - * - * @var \Laravel\Lumen\Application|mixed - */ - protected $graphQL; - - /** - * GraphQLType constructor. - * - */ - public function __construct() - { - parent::__construct(); - - $this->graphQL = app('graphql'); - } - /** * Arguments this field accepts. * @@ -61,7 +43,7 @@ public function type() } /** - * Rules to apply to mutation. + * Rules to apply to field. * * @return array */ @@ -89,15 +71,20 @@ public function getAttributes() } /** - * Get rules for mutation. + * Get rules for field. * * @return array */ public function getRules() { $arguments = func_get_args(); + $args = $this->args(); + + if ($this instanceof RelayMutation) { + $args = $this->inputFields(); + } - return collect($this->args()) + return collect($args) ->transform(function ($arg, $name) use ($arguments) { if (isset($arg['rules'])) { if (is_callable($arg['rules'])) { @@ -115,7 +102,7 @@ public function getRules() } /** - * Get the mutation resolver. + * Get the field resolver. * * @return \Closure|null */ @@ -132,8 +119,8 @@ protected function getResolver() $rules = call_user_func_array([$this, 'getRules'], $arguments); if (sizeof($rules)) { - $args = array_get($arguments, 1, []); - $validator = app('validator')->make($args, $rules); + $input = $this->getInput($arguments); + $validator = app('validator')->make($input, $rules); if ($validator->fails()) { throw with(new ValidationError('validation'))->setValidator($validator); @@ -144,6 +131,17 @@ protected function getResolver() }; } + /** + * Get input for validation. + * + * @param array $arguments + * @return array + */ + protected function getInput(array $arguments) + { + return array_get($arguments, 1, []); + } + /** * Convert the Fluent instance to an array. * diff --git a/src/Support/Definition/RelayMutation.php b/src/Support/Definition/RelayMutation.php index dad351e..7a8bdb8 100644 --- a/src/Support/Definition/RelayMutation.php +++ b/src/Support/Definition/RelayMutation.php @@ -21,6 +21,13 @@ abstract class RelayMutation extends GraphQLMutation */ protected $mutatesRelayType = true; + /** + * Mutation id sent from client. + * + * @var integer|null + */ + protected $clientMutationId = null; + /** * Generate Relay compliant output type. * @@ -32,7 +39,10 @@ public function type() 'name' => ucfirst($this->name()) . 'Payload', 'fields' => array_merge($this->outputFields(), [ 'clientMutationId' => [ - 'type' => Type::nonNull(Type::string()) + 'type' => Type::nonNull(Type::string()), + 'resolve' => function () { + return $this->clientMutationId; + } ] ]) ]); @@ -76,76 +86,20 @@ public function resolve($_, $args, ResolveInfo $info) $args['input']['id'] = $this->decodeRelayId($args['input']['id']); } - $this->validateMutation($args); - $payload = $this->mutateAndGetPayload($args['input'], $info); + $this->clientMutationId = $args['input']['clientMutationId']; - return array_merge($payload, [ - 'clientMutationId' => $args['input']['clientMutationId'] - ]); + return $this->mutateAndGetPayload($args['input'], $info); } /** - * Get rules for relay mutation. + * Get input for validation. * + * @param array $arguments * @return array */ - public function getRules() - { - $arguments = func_get_args(); - - $rules = call_user_func_array([$this, 'rules'], $arguments); - $argsRules = []; - foreach ($this->inputFields() as $name => $arg) { - if (isset($arg['rules'])) { - if (is_callable($arg['rules'])) { - $argsRules[$name] = call_user_func_array($arg['rules'], $arguments); - } else { - $argsRules[$name] = $arg['rules']; - } - } - } - - return array_merge($argsRules, $rules); - } - - /** - * Get resolver for relay mutation. - * - * @return mixed - */ - protected function getResolver() + protected function getInput(array $arguments) { - if (!method_exists($this, 'resolve')) { - return null; - } - - $resolver = array($this, 'resolve'); - - return function () use ($resolver) { - $arguments = func_get_args(); - - return call_user_func_array($resolver, $arguments); - }; - } - - /** - * Validate relay mutation. - * - * @param array $args - * @throws ValidationError - * @return void - */ - protected function validateMutation(array $args) - { - $rules = call_user_func_array([$this, 'getRules'], $args); - - if (sizeof($rules)) { - $validator = Validator::make($args['input'], $rules); - - if ($validator->fails()) { - throw with(new ValidationError('validation'))->setValidator($validator); - } - } + return array_get($arguments, '1.input', []); } /** From 8c69950f2dc62c8d54f5af61f2005fdbc93dd5d9 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Wed, 9 Mar 2016 16:23:33 -0700 Subject: [PATCH 55/66] add query function to relay trait --- src/Traits/RelayMiddleware.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Traits/RelayMiddleware.php b/src/Traits/RelayMiddleware.php index 601aba3..17d9edc 100644 --- a/src/Traits/RelayMiddleware.php +++ b/src/Traits/RelayMiddleware.php @@ -23,4 +23,22 @@ public function setupQuery(Request $request) $this->middleware($middleware); } } + + /** + * Process GraphQL query. + * + * @param Request $request + * @return Response + */ + public function graphqlQuery(Request $request) + { + $query = $request->get('query'); + $params = $request->get('variables'); + + if (is_string($params)) { + $params = json_decode($params, true); + } + + return app('graphql')->query($query, $params); + } } From 80257b255d69ab84038e3eecda37c666335f9d53 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Wed, 9 Mar 2016 16:52:32 -0700 Subject: [PATCH 56/66] added default route/controller --- src/Commands/stubs/field.stub | 2 +- src/{ => Http}/Controllers/LaravelController.php | 15 ++++++++++++++- src/{ => Http}/Controllers/LumenController.php | 4 ++-- src/Http/routes.php | 5 +++++ src/LaravelServiceProvider.php | 4 ++++ 5 files changed, 26 insertions(+), 4 deletions(-) rename src/{ => Http}/Controllers/LaravelController.php (62%) rename src/{ => Http}/Controllers/LumenController.php (85%) create mode 100644 src/Http/routes.php diff --git a/src/Commands/stubs/field.stub b/src/Commands/stubs/field.stub index 2707abf..3203f1f 100644 --- a/src/Commands/stubs/field.stub +++ b/src/Commands/stubs/field.stub @@ -25,7 +25,7 @@ class DummyClass extends GraphQLField */ public function type() { - // return GraphQL::type('type'); + // return Type::string(); } /** diff --git a/src/Controllers/LaravelController.php b/src/Http/Controllers/LaravelController.php similarity index 62% rename from src/Controllers/LaravelController.php rename to src/Http/Controllers/LaravelController.php index ab1e887..aec659c 100644 --- a/src/Controllers/LaravelController.php +++ b/src/Http/Controllers/LaravelController.php @@ -1,12 +1,25 @@ setupQuery($request); + } + /** * Execute GraphQL query. * diff --git a/src/Controllers/LumenController.php b/src/Http/Controllers/LumenController.php similarity index 85% rename from src/Controllers/LumenController.php rename to src/Http/Controllers/LumenController.php index 889e98d..621b3df 100644 --- a/src/Controllers/LumenController.php +++ b/src/Http/Controllers/LumenController.php @@ -1,11 +1,11 @@ 'graphql', 'uses' => $controller]); diff --git a/src/LaravelServiceProvider.php b/src/LaravelServiceProvider.php index 03e7024..57fb421 100644 --- a/src/LaravelServiceProvider.php +++ b/src/LaravelServiceProvider.php @@ -27,6 +27,10 @@ public function boot() $this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'relay'); $this->registerSchema(); + + if (config('relay.controller')) { + include __DIR__.'/Http/routes.php'; + } } /** From 8d32bf4e59e316f9d3cb7f06abe878a6c9c137e7 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Wed, 9 Mar 2016 16:53:38 -0700 Subject: [PATCH 57/66] add controller to config --- config/config.php | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.php b/config/config.php index 3c4a970..d6ce673 100644 --- a/config/config.php +++ b/config/config.php @@ -40,6 +40,7 @@ 'queries' => [] ], + 'controller' => 'Nuwave\Relay\Http\Controllers\LaravelController@query', 'model_path' => 'App\\Models', 'camel_case' => false, ]; From 6b07b2b6bc2ab381954232095ad8ed1a5cc3c6a4 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Wed, 9 Mar 2016 17:05:14 -0700 Subject: [PATCH 58/66] correct path to introspection query --- src/Support/SchemaGenerator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Support/SchemaGenerator.php b/src/Support/SchemaGenerator.php index 1b93a29..141232a 100644 --- a/src/Support/SchemaGenerator.php +++ b/src/Support/SchemaGenerator.php @@ -14,7 +14,7 @@ class SchemaGenerator */ public function execute($version = '4.12') { - $query = file_get_contents(dirname(__DIR__) . '/assets/introspection-'. $version .'.txt'); + $query = file_get_contents(realpath(__DIR__.'/../../assets').'/introspection-'. $version .'.txt'); $data = GraphQL::query($query); if (isset($data['data']['__schema'])) { From a022444fa622c2dcbe1e00dac6216331c8803492 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Fri, 11 Mar 2016 13:05:38 -0700 Subject: [PATCH 59/66] inital set of revised docs --- docs/Configuration.md | 52 +++++++ docs/Overview.md | 40 +++++ docs/Schema.md | 334 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 426 insertions(+) create mode 100644 docs/Configuration.md create mode 100644 docs/Overview.md create mode 100644 docs/Schema.md diff --git a/docs/Configuration.md b/docs/Configuration.md new file mode 100644 index 0000000..61197f1 --- /dev/null +++ b/docs/Configuration.md @@ -0,0 +1,52 @@ +## Configuration ## + +When publishing the configuration, the package will create a ```relay.php``` file in your ```config``` folder. + +### Namespaces ### + +```php +'namespaces' => [ + 'mutations' => 'App\\GraphQL\\Mutations', + 'queries' => 'App\\GraphQL\\Queries', + 'types' => 'App\\GraphQL\\Types', + 'fields' => 'App\\GraphQL\\Fields', +], +``` + +This package provides a list of commands that allows you to create Types, Mutations, Queries and Fields. You can specify the namespaces you would like the package to use when generating the files. + +### Schema ### + +```php +'schema' => [ + 'file' => 'Http/GraphQL/schema.php', + 'output' => null, +] +``` + +** File ** + +Set the location of your schema file. (A schema is similar to your routes.php file and defines your Types, Mutations and Queries for GraphQL. Read More) + +** Output ** + +This is the location where your generated ```schema.json``` will be created/updated. (This json file is used by the [Babel Relay Plugin](https://facebook.github.io/relay/docs/guides-babel-plugin.html#content)). + +### Eloquent ### + +```php +'eloquent' => [ + 'path' => 'App\\Models', + 'camel_case' => false +] +``` + +** Path ** + +The package allows you to create Types based off of your Eloquent models. You can use the ```path``` to define the namespace of your models or you can use the full namespace when generating Types from the console (Read More). + +** Camel Case ** + +Camel casing is quite common in javascript, but Laravel's database column naming convention is snake case. If you would like your Eloquent model's generated fields converted to camel case, you may set this to true. + +*This works great with the [Eloquence package](https://github.com/kirkbushell/eloquence).* diff --git a/docs/Overview.md b/docs/Overview.md new file mode 100644 index 0000000..99a2eec --- /dev/null +++ b/docs/Overview.md @@ -0,0 +1,40 @@ +# laravel-grapql-relay # + +Use Facebook [GraphQL](http://facebook.github.io/graphql/) with [React Relay](https://facebook.github.io/relay/). This package extends graphql-php to work with Laravel and is currently **a work in progress**. You can reference what specifications GraphQL needs to provide to work with Relay in the [documentation](https://facebook.github.io/relay/docs/graphql-relay-specification.html#content). + +### Installation ### + +You must then modify your composer.json file and run composer update to include the latest version of the package in your project. + +```php +"require": { + "nuwave/laravel-graphql-relay": "0.3.*" +} +``` + +Or you can use the composer require command from your terminal. + +``` +composer require nuwave/laravel-graphql-relay +``` + +Add the service provider to your ```app/config.php``` file + +``` +Nuwave\Relay\LaravelServiceProvider::class +``` + +Add the Relay & GraphQL facade to your app/config.php file + +``` +'GraphQL' => Nuwave\Relay\Facades\GraphQL::class, +'Relay' => Nuwave\Relay\Facades\Relay::class, +``` + +Publish the configuration file + +``` +php artisan vendor:publish --provider="Nuwave\Relay\LaravelServiceProvider" +``` + +For additional documentation, look through the docs folder or read the Wiki. diff --git a/docs/Schema.md b/docs/Schema.md new file mode 100644 index 0000000..e239e11 --- /dev/null +++ b/docs/Schema.md @@ -0,0 +1,334 @@ +## Schema + +### Types + +Creating a Type: + +``` +php artisan make:relay:type UserType +``` + +```php + 'User', + 'description' => 'A user of the application.', + ]; + + /** + * Get user by id. + * + * @param string $id + * @return User + */ + public function resolveById($id) + { + return User::find($id); + } + + /** + * Available fields of Type. + * + * @return array + */ + public function relayFields() + { + return [ + 'id' => [ + 'type' => Type::nonNull(Type::id()), + 'description' => 'The primary id of the user.' + ], + 'name' => [ + 'type' => Type::string(), + 'description' => 'Full name of user.' + ], + 'email' => [ + 'type' => Type::string(), + 'description' => 'Email address of user.' + ] + // ... + ] + } +} +``` + +### Queries + +Create a Query: + +```bash +php artisan make:relay:query UserQuery +``` + +```php + [ + 'type' => Type::nonNull(Type::string()), + ] + ]; + } + + /** + * Resolve the query. + * + * @param mixed $root + * @param array $args + * @return mixed + */ + public function resolve($root, array $args) + { + return User::find($args['id']); + } +} + +``` + +### Mutations + +Create a mutation: + +```bash +php artisan make:relay:mutation +``` + +```php + [ + 'type' => Type::string(), + 'rules' => ['required'] + ], + 'password' => [ + 'type' => Type::string() + ] + ]; + } + + /** + * Rules for mutation. + * + * Note: You can add your rules here or define + * them in the inputFields + * + * @return array + */ + public function rules() + { + return [ + 'password' => ['required', 'min:15'] + ]; + } + + /** + * Fields that will be sent back to client. + * + * @return array + */ + protected function outputFields() + { + return [ + 'user' => [ + 'type' => GraphQL::type('user'), + 'resolve' => function (User $user) { + return $user; + } + ] + ]; + } + + /** + * Perform data mutation. + * + * @param array $input + * @param ResolveInfo $info + * @return array + */ + protected function mutateAndGetPayload(array $input, ResolveInfo $info) + { + $user = User::find($input['id']); + $user->password = \Hash::make($input['password']); + $user->save(); + + return $user; + } +} + +``` + +### Custom Fields + +Create a custom field: + +```bash +php artisan relay:make:field AvatarField +``` + +```php + 'Avatar of user.' + ]; + + /** + * The return type of the field. + * + * @return Type + */ + public function type() + { + return Type::string(); + } + + /** + * Available field arguments. + * + * @return array + */ + public function args() + { + return [ + 'width' => [ + 'type' => Type::int(), + 'description' => 'The width of the picture' + ], + 'height' => [ + 'type' => Type::int(), + 'description' => 'The height of the picture' + ] + ]; + } + + /** + * Resolve the field. + * + * @param mixed $root + * @param array $args + * @return mixed + */ + public function resolve($root, array $args) + { + $width = isset($args['width']) ? $args['width'] : 100; + $height = isset($args['height']) ? $args['height'] : 100; + + return 'http://placehold.it/'.$root->id.'/'.$width.'x'.$height; + } +} +``` + +### Schema File + +The ```schema.php``` file you create is similar to Laravel's ```routes.php``` file. It used to declare your Types, Mutations and Queries to be used by GraphQL. Similar to routes, you can group your schema by namespace as well as add middleware to your Queries and Mutations. + +*Be sure your file name is located in the ```relay.php``` config file* + +```php +// config/relay.php + +'schema' => [ + 'path' => 'Http/schema.php', + 'output' => null +], +``` + +```php +// app/Http/schema.php + +Relay::group(['namespace' => 'App\\Http\\GraphQL', 'middleware' => 'auth'], function () { + Relay::group(['namespace' => 'Mutations'], function () { + Relay::mutation('createUser', 'CreateUserMutation'); + }); + + Relay::group(['namespace' => 'Queries'], function () { + Relay::query('userQuery', 'UserQuery'); + }); + + Relay::group(['namespace' => 'Types'], function () { + Relay::type('user', 'UserType'); + }); +}); +``` From aca461301f6df37ceb583bd9dce3bc1e62d0a793 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 12 Mar 2016 07:02:39 -0700 Subject: [PATCH 60/66] drop query select this is a really nice feature that we can hopefully bring back soon, but there is currently an issue with deeply nested nodes --- src/Support/ConnectionResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Support/ConnectionResolver.php b/src/Support/ConnectionResolver.php index 28df7e7..8958f87 100644 --- a/src/Support/ConnectionResolver.php +++ b/src/Support/ConnectionResolver.php @@ -58,7 +58,7 @@ protected function getItems($collection, ResolveInfo $info, $name) if ($collection instanceof Model) { // Selects only the fields requested, instead of select * $items = method_exists($collection, $name) - ? $collection->$name()->select(...$this->getSelectFields($info))->get() + ? $collection->{$name}()->get() //->select(...$this->getSelectFields($info))->get() : $collection->getAttribute($name); return $items; } elseif (is_object($collection) && method_exists($collection, 'get')) { From 08c98c6d9f63ab53b22c3625fe6717523ea763f8 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 12 Mar 2016 07:07:47 -0700 Subject: [PATCH 61/66] add eager loading helper method --- src/Schema/SchemaContainer.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Schema/SchemaContainer.php b/src/Schema/SchemaContainer.php index 8a19485..669735c 100644 --- a/src/Schema/SchemaContainer.php +++ b/src/Schema/SchemaContainer.php @@ -378,11 +378,21 @@ public function middleware() /** * Get connections for the query. * - * @return array + * @return \Illuminate\Support\Collection */ public function connections() { - return $this->connections; + return collect($this->connections); + } + + /** + * Get connection paths to eager load. + * + * @return array + */ + public function eagerLoad() + { + return $this->connections()->pluck('path')->toArray(); } /** From 8126181bd6f0826173fef71875c3b936faa38f5d Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 12 Mar 2016 07:25:57 -0700 Subject: [PATCH 62/66] use eager loaded relations --- src/Support/ConnectionResolver.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Support/ConnectionResolver.php b/src/Support/ConnectionResolver.php index 8958f87..14df096 100644 --- a/src/Support/ConnectionResolver.php +++ b/src/Support/ConnectionResolver.php @@ -56,7 +56,10 @@ protected function getItems($collection, ResolveInfo $info, $name) $items = []; if ($collection instanceof Model) { - // Selects only the fields requested, instead of select * + if (in_array($name, array_keys($collection->getRelations()))) { + return $collection->{$name}; + } + $items = method_exists($collection, $name) ? $collection->{$name}()->get() //->select(...$this->getSelectFields($info))->get() : $collection->getAttribute($name); From 17244083251ee73e47b0cd41ab383ac1097c3177 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 12 Mar 2016 07:35:30 -0700 Subject: [PATCH 63/66] add relay documentation --- docs/Relay.md | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 docs/Relay.md diff --git a/docs/Relay.md b/docs/Relay.md new file mode 100644 index 0000000..78eeefc --- /dev/null +++ b/docs/Relay.md @@ -0,0 +1,130 @@ +## Object Identification + +Facebook Relay [Documentation](https://facebook.github.io/relay/docs/graphql-object-identification.html#content) + +Facebook GraphQL [Spec](https://facebook.github.io/relay/graphql/objectidentification.htm) + +To implement a GraphQL Type that adheres to the Relay Object Identification spec, make sure your type extends ```Nuwave\Relay\Support\Definition\RelayType``` and implements the ```resolveById``` and ```relayFields``` methods. + +Example: + +```php + 'Customer', + 'description' => 'A Customer model.', + ]; + + /** + * Get customer by id. + * + * When the root 'node' query is called, it will use this method + * to resolve the type by providing the id. + * + * @param string $id + * @return Customer + */ + public function resolveById($id) + { + return Customer::find($id); + } + + /** + * Available fields of Type. + * + * @return array + */ + public function relayFields() + { + return [ + // Note: You may omit the id field as it will be overwritten to adhere to + // the NodeInterface + 'id' => [ + 'type' => Type::nonNull(Type::id()), + 'description' => 'ID of the customer.' + ], + // ... + ]; + } +} +``` + +## Connections + +Facebook Relay [Documentation](https://facebook.github.io/relay/docs/graphql-connections.html#content) + +Facebook GraphQL [Spec](https://facebook.github.io/relay/graphql/connections.htm) + +To create a connection, simply use ```GraphQL::connection('typeName', Closure)```. We need to pass back an object that [implements the ```Illuminate\Contract\Pagination\LengthAwarePaginator``` interface](http://laravel.com/api/5.1/Illuminate/Contracts/Pagination/LengthAwarePaginator.html). In this example, we'll add it to our CustomerType we created in the Object Identification section. + +*(You can omit the resolve function if you are working with an Eloquent model. The package will use the same code as show below to resolve the connection.)* + +Example: + +```php + GraphQL::connection('order', function ($customer, array $args, ResolveInfo $info) { + // Note: This is just an example. This type of resolve functionality may not make sense for your + // application so just use what works best for you. However, you will need to pass back an object + // that implements the LengthAwarePaginator as mentioned above in order for it to work with the + // Relay connection spec. + $orders = $customer->orders; + + if (isset($args['first'])) { + $total = $orders->count(); + $first = $args['first']; + $after = $this->decodeCursor($args); + $currentPage = $first && $after ? floor(($first + $after) / $first) : 1; + + return new Paginator( + $orders->slice($after)->take($first), + $total, + $first, + $currentPage + ); + } + + return new Paginator( + $orders, + $orders->count(), + $orders->count() + ); + }), + // Alternatively, you can let the package resolve this connection for you + // by passing the name of the relationship. + 'orders' => GraphQL::connection('order', 'orders') + ]; + } +} +``` From da1c8acd9a8140a720f9f4d5afd6cdcb17e9c9a7 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 12 Mar 2016 07:55:45 -0700 Subject: [PATCH 64/66] add schema generation to overview --- config/config.php | 3 --- docs/Overview.md | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/config/config.php b/config/config.php index d6ce673..b05ddf9 100644 --- a/config/config.php +++ b/config/config.php @@ -35,9 +35,6 @@ 'schema' => [ 'path' => null, 'output' => null, - 'types' => [], - 'mutations' => [], - 'queries' => [] ], 'controller' => 'Nuwave\Relay\Http\Controllers\LaravelController@query', diff --git a/docs/Overview.md b/docs/Overview.md index 99a2eec..7357504 100644 --- a/docs/Overview.md +++ b/docs/Overview.md @@ -37,4 +37,23 @@ Publish the configuration file php artisan vendor:publish --provider="Nuwave\Relay\LaravelServiceProvider" ``` +Create a ```schema.php``` file and add the path to the config + +``` +// config/relay.php +// ... +'schema' => [ + 'path' => 'Http/schema.php', + 'output' => null, +], +``` + +To generate a ```schema.json``` file (used with the Babel Relay Plugin): + +``` +php artisan relay:schema +``` + +*You can customize the output path in the ```relay.php``` config file under ```schema.output```* + For additional documentation, look through the docs folder or read the Wiki. From 6fb6069842f0baa7374c171f1d97b41c6af9bf4c Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 12 Mar 2016 08:04:30 -0700 Subject: [PATCH 65/66] update documentation --- docs/Overview.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/Overview.md b/docs/Overview.md index 7357504..f065865 100644 --- a/docs/Overview.md +++ b/docs/Overview.md @@ -2,6 +2,10 @@ Use Facebook [GraphQL](http://facebook.github.io/graphql/) with [React Relay](https://facebook.github.io/relay/). This package extends graphql-php to work with Laravel and is currently **a work in progress**. You can reference what specifications GraphQL needs to provide to work with Relay in the [documentation](https://facebook.github.io/relay/docs/graphql-relay-specification.html#content). +Although this package no longer depends on [laraval-graphql](https://github.com/Folkloreatelier/laravel-graphql), it laid the foundation for this package which likely wouldn't exist without it. It is also a great alternative if you are using GraphQL w/o support for Relay. + +Because this package is still in the early stages, breaking changes will occur. We will keep the documentation updated with the current release. Please feel free to contribute, PR are absolutely welcome! + ### Installation ### You must then modify your composer.json file and run composer update to include the latest version of the package in your project. From 330d8bbefab58bb54832c1f7184174ba3b173755 Mon Sep 17 00:00:00 2001 From: Christopher Moore Date: Sat, 12 Mar 2016 08:12:55 -0700 Subject: [PATCH 66/66] replace readme --- README.md | 61 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 49bf9d2..f065865 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,63 @@ -# laravel-graphql-relay +# laravel-grapql-relay # -## Documentation currently under development +Use Facebook [GraphQL](http://facebook.github.io/graphql/) with [React Relay](https://facebook.github.io/relay/). This package extends graphql-php to work with Laravel and is currently **a work in progress**. You can reference what specifications GraphQL needs to provide to work with Relay in the [documentation](https://facebook.github.io/relay/docs/graphql-relay-specification.html#content). -Use Facebook [GraphQL](http://facebook.github.io/graphql/) with [React Relay](https://facebook.github.io/relay/). This package is used alongside [laravel-graphql](https://github.com/Folkloreatelier/laravel-graphql) and is currently **a work in progress**. You can reference what specifications GraphQL needs to provide to work with Relay in the [documentation](https://facebook.github.io/relay/docs/graphql-relay-specification.html#content). +Although this package no longer depends on [laraval-graphql](https://github.com/Folkloreatelier/laravel-graphql), it laid the foundation for this package which likely wouldn't exist without it. It is also a great alternative if you are using GraphQL w/o support for Relay. -## Installation +Because this package is still in the early stages, breaking changes will occur. We will keep the documentation updated with the current release. Please feel free to contribute, PR are absolutely welcome! + +### Installation ### You must then modify your composer.json file and run composer update to include the latest version of the package in your project. -```json +```php "require": { - "nuwave/laravel-graphql-relay": "0.1.*" + "nuwave/laravel-graphql-relay": "0.3.*" } ``` -Or you can use the ```composer require``` command from your terminal. +Or you can use the composer require command from your terminal. -```json +``` composer require nuwave/laravel-graphql-relay ``` Add the service provider to your ```app/config.php``` file -```php -Nuwave\Relay\ServiceProvider::class +``` +Nuwave\Relay\LaravelServiceProvider::class ``` -Add the following entries to your ```config/graphql.php``` file ([laravel-graphql configuration file](https://github.com/Folkloreatelier/laravel-graphql#installation-1)) +Add the Relay & GraphQL facade to your app/config.php file -```php +``` +'GraphQL' => Nuwave\Relay\Facades\GraphQL::class, +'Relay' => Nuwave\Relay\Facades\Relay::class, +``` + +Publish the configuration file + +``` +php artisan vendor:publish --provider="Nuwave\Relay\LaravelServiceProvider" +``` + +Create a ```schema.php``` file and add the path to the config + +``` +// config/relay.php +// ... 'schema' => [ - 'query' => [ - // ... - 'node' => Nuwave\Relay\Node\NodeQuery::class, - ], - // ... + 'path' => 'Http/schema.php', + 'output' => null, ], -'types' => [ - // ... - 'node' => Nuwave\Relay\Node\NodeType::class, - 'pageInfo' => Nuwave\Relay\Types\PageInfoType::class, -] ``` -To generate a ```schema.json``` file (used with the [Babel Relay Plugin](https://facebook.github.io/relay/docs/guides-babel-plugin.html#content)) +To generate a ```schema.json``` file (used with the Babel Relay Plugin): -```php +``` php artisan relay:schema ``` -For additional documentation, please read the [Wiki](https://github.com/nuwave/laravel-graphql-relay/wiki/1.-GraphQL-and-Relay) +*You can customize the output path in the ```relay.php``` config file under ```schema.output```* + +For additional documentation, look through the docs folder or read the Wiki.