diff --git a/.gitignore b/.gitignore index cd19dd0..f77ee14 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /vendor /.php_cs.cache *.orig +/.idea \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 98da62e..15c6803 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,29 +1,30 @@ -language : php -php : [7.1, 7.2] -cache : { directories : [$COMPOSER_CACHE_DIR, $HOME/.composer/cache, vendor] } -install : composer update --no-interaction --prefer-dist +language : php +php : [7.1, 7.2] +cache : { directories : [$COMPOSER_CACHE_DIR, $HOME/.composer/cache, vendor] } +install : composer update --no-interaction --prefer-dist notifications : - email : false + email : false stages : - - test - - lint + - test + - lint script : - - vendor/bin/phpunit + - vendor/bin/phpunit before_install : - - composer global require hirak/prestissimo --update-no-dev + - composer global require hirak/prestissimo --update-no-dev jobs : - include : - - stage : lint - php : 7.2 - before_install : - - composer global require hirak/prestissimo --update-no-dev - - composer require phpmd/phpmd --no-update --prefer-dist - - composer require phpstan/phpstan --no-update --prefer-dist - script : - - vendor/bin/phpmd src text phpmd.xml - - vendor/bin/phpmd tests text phpmd.xml - - vendor/bin/phpstan analyse --autoload-file=_ide_helper.php --level 1 src + include : + - stage : lint + php : 7.2 + env : TESTBENCH_VERSION=3.6.* LARAVEL_VERSION=5.6.* + before_install : + - composer global require hirak/prestissimo --update-no-dev + - composer require phpmd/phpmd --no-update --prefer-dist + - composer require phpstan/phpstan --no-update --prefer-dist + script : + - vendor/bin/phpmd src text phpmd.xml + - vendor/bin/phpmd tests text phpmd.xml + - vendor/bin/phpstan analyse --autoload-file=_ide_helper.php --level 1 src diff --git a/README.md b/README.md index 05301d8..4f0d9e9 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,10 @@ php artisan vendor:publish --provider="StudioNet\GraphQL\ServiceProvider" - [Definition](#definition) - [Query](#query) - [Mutation](#mutation) +- [Require authorization](#require-authorization) - [Self documentation](#self-documentation) - [Examples](#examples) +- [N+1 Problem](#n+1-problem) ### Definition @@ -50,6 +52,7 @@ use StudioNet\GraphQL\Definition\Type; use StudioNet\GraphQL\Support\Definition\EloquentDefinition; use StudioNet\GraphQL\Filter\EqualsOrContainsFilter; use App\User; +use Auth; /** * Specify user GraphQL definition @@ -184,21 +187,38 @@ namespace App\GraphQL\Query; use StudioNet\GraphQL\Support\Definition\Query; use Illuminate\Support\Facades\Auth; +use App\User; +use Auth; class Viewer extends Query { + /** + * {@inheritDoc} + */ + protected function authorize(array $args) { + // check, that user is not a guest + return !Auth::guest(); + } + /** * {@inheritDoc} */ public function getRelatedType() { return \GraphQL::type('user'); } + + /** + * {@inheritdoc} + */ + public function getSource() { + return User::class; + } /** * Return logged user * - * @return \App\User|null + * @return User|null */ - public function getResolver() { + public function getResolver($opts) { return Auth::user(); } } @@ -222,6 +242,15 @@ return [ ]; ``` +`getResolver()` receives an array-argument with followed item: + +- `root` 1st argument given by webonyx library - `GraphQL\Executor\Executor::resolveOrError()` +- `args` 2nd argument given by webonyx library +- `context` 3rd argument given by webonyx library +- `info` 4th argument given by webonyx library +- `fields` array of fields, that were fetched from query. Limited by depth in `StudioNet\GraphQL\GraphQL::FIELD_SELECTION_DEPTH` +- `with` array of relations, that could/should be eager loaded. **NOTICE:** Finding this relations happens ONLY, if `getSource()` is defined - this method should return a class name of a associated root-type in query. If `getSource()` is not defined, then `with` will be always empty. + ### Mutation Mutation are used to update or create data. @@ -236,6 +265,14 @@ use StudioNet\GraphQL\Definition\Type; use App\User; class Profile extends Mutation { + /** + * {@inheritDoc} + */ + protected function authorize(array $args) { + // check, that user is not a guest + return !Auth::guest(); + } + /** * {@inheritDoc} * @@ -294,6 +331,14 @@ return [ ]; ``` +### Require authorization + +Currently you have a possibility to protect your own queries and mutations. You have to implement `authorize()` method in your query/mutation, that return a boolean, that indicates, if requested query/mutation has to be executed. If method return `false`, an `UNAUTHORIZED` GraphQL-Error will be thrown. + +Usage examples are in query and mutation above. + +Protection of definition transformers are currently not implemented, but may be will in the future. By now you have to define your query/mutation yourself, and protect it then with logic in `authorize()`. + ### Self documentation A documentation generator is implemented with the package. By default, you can access it by navigate to `/doc/graphql`. You can change this behavior within the configuration file. The built-in documentation is implemented from [this repository](https://github.com/mhallin/graphql-docs). @@ -560,6 +605,14 @@ post-save (which can be useful for eloquent relational models) : } ``` +### N+1 Problem + +The common question is, if graphql library solves n+1 problem. This occures, when graphql resolves relation. Often entities are fetched without relations, and when graphql query needs to fetch relation, for each fetched entity relation would be fetched from SQL separately. So instead of executing 2 SQL queries, you will get N+1 queries, where N is the count of results of root entity. In that example you would query only one relation. If you query more relations, then it becomes N^2+1 problem. + +To solve it, Eloquent has already options to eager load relations. Transformers in this library use eager loading, depends on what you query. + +Currently this smart detection works perfect only on View and List Transformers. Other transformers will be reworked soon. + ## Contribution If you want participate to the project, thank you ! In order to work properly, diff --git a/database/factories/Comment.php b/database/factories/Comment.php new file mode 100644 index 0000000..abbe4bf --- /dev/null +++ b/database/factories/Comment.php @@ -0,0 +1,9 @@ +define(Entity\Comment::class, function (Generator $faker) { + return [ + 'body' => $faker->text(100) + ]; +}); diff --git a/database/factories/Country.php b/database/factories/Country.php new file mode 100644 index 0000000..0329483 --- /dev/null +++ b/database/factories/Country.php @@ -0,0 +1,9 @@ +define(Entity\Country::class, function (Generator $faker) { + return [ + 'name' => $faker->country + ]; +}); diff --git a/database/factories/Label.php b/database/factories/Label.php new file mode 100644 index 0000000..b5f0eff --- /dev/null +++ b/database/factories/Label.php @@ -0,0 +1,9 @@ +define(Entity\Label::class, function (Generator $faker) { + return [ + 'name' => $faker->word + ]; +}); diff --git a/database/factories/Phone.php b/database/factories/Phone.php new file mode 100644 index 0000000..0631cbf --- /dev/null +++ b/database/factories/Phone.php @@ -0,0 +1,10 @@ +define(Entity\Phone::class, function (Generator $faker) { + return [ + 'label' => $faker->word, + 'number' => $faker->phoneNumber + ]; +}); diff --git a/database/migrations/2018_08_07_021200_create_phones_table.php b/database/migrations/2018_08_07_021200_create_phones_table.php new file mode 100644 index 0000000..3dd5508 --- /dev/null +++ b/database/migrations/2018_08_07_021200_create_phones_table.php @@ -0,0 +1,33 @@ +increments('id'); + + $table->string('label'); + $table->string('number'); + $table->unsignedInteger('user_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() { + Schema::drop('phones'); + } +} diff --git a/database/migrations/2018_08_07_022900_create_countries_table_and_extend_users_table.php b/database/migrations/2018_08_07_022900_create_countries_table_and_extend_users_table.php new file mode 100644 index 0000000..1377a24 --- /dev/null +++ b/database/migrations/2018_08_07_022900_create_countries_table_and_extend_users_table.php @@ -0,0 +1,39 @@ +increments('id'); + $table->string('name'); + }); + + Schema::table('users', function (Blueprint $table) { + $table->unsignedInteger('country_id')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('country_id'); + }); + Schema::drop('phones'); + } +} diff --git a/database/migrations/2018_08_08_144100_create_comments_table.php b/database/migrations/2018_08_08_144100_create_comments_table.php new file mode 100644 index 0000000..1063d0f --- /dev/null +++ b/database/migrations/2018_08_08_144100_create_comments_table.php @@ -0,0 +1,35 @@ +increments('id'); + $table->text('body'); + $table->unsignedInteger('commentable_id'); + $table->string('commentable_type'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() { + Schema::drop('comments'); + } +} diff --git a/database/migrations/2018_08_08_153400_create_labels_and_labelables_tables.php b/database/migrations/2018_08_08_153400_create_labels_and_labelables_tables.php new file mode 100644 index 0000000..f1dd2ca --- /dev/null +++ b/database/migrations/2018_08_08_153400_create_labels_and_labelables_tables.php @@ -0,0 +1,38 @@ +increments('id'); + $table->text('name'); + }); + Schema::create('labelables', function (Blueprint $table) { + $table->unsignedInteger('label_id'); + $table->unsignedInteger('labelable_id'); + $table->string('labelable_type'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() { + Schema::drop('labelables'); + Schema::drop('labels'); + } +} diff --git a/src/GraphQL.php b/src/GraphQL.php index 1771963..65a0e6b 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -7,6 +7,7 @@ use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type as Type; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Foundation\Application; use StudioNet\GraphQL\Cache\CachePool; use StudioNet\GraphQL\Support\Definition\Definition; @@ -25,6 +26,9 @@ class GraphQL { /** @var CachePool $cache */ private $cache; + /** @var int Depth-level, how deep fields will be fetched for relations guessing */ + const FIELD_SELECTION_DEPTH = 3; + /** * __construct * @@ -328,4 +332,36 @@ private function get($namespace, $key = null) { private function make($cls) { return (is_string($cls)) ? $this->app->make($cls) : $cls; } + + /** + * Return relationship based on fields that are queried + * + * @param Model $model + * @param array $fields + * @param string $parentRelation + * + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public static function guessWithRelations(Model $model, array $fields, string $parentRelation = null) { + $relations = []; + // Parse each field in order to retrieve relationship elements on root + // of array (as relationship are based upon multiple resolvers, we just + // have to handle the root fields here) + foreach ($fields as $key => $field) { + if (is_array($field) && method_exists($model, $key)) { + // verify, that given method returns relation + $relation = call_user_func([$model, $key]); + if ($relation instanceof Relation) { + $relationNameToStore = $parentRelation ? "{$parentRelation}.{$key}" : $key; + $relations[] = $relationNameToStore; + + // also guess relations for found relation + $relations = array_merge($relations, self::guessWithRelations($relation->getModel(), $field, $relationNameToStore)); + } + } + } + + return $relations; + } } diff --git a/src/Support/Definition/Definition.php b/src/Support/Definition/Definition.php index f4a4ae2..84553f5 100644 --- a/src/Support/Definition/Definition.php +++ b/src/Support/Definition/Definition.php @@ -81,7 +81,7 @@ public function getTransformers() { /** * Returns availabled pipes * - * @return void + * @return array */ public function getPipes(): array { return []; diff --git a/src/Support/Definition/Field.php b/src/Support/Definition/Field.php index 2c2d884..155408a 100644 --- a/src/Support/Definition/Field.php +++ b/src/Support/Definition/Field.php @@ -1,7 +1,12 @@ app = $app; + } + /** * Return field name * @@ -42,6 +72,16 @@ public function getArguments() { return []; } + /** + * An optional definition of source model for root of the query/mutation. + * Used to resolve eager loading in custom queries. + * + * @return Model|null + */ + public function getSource() { + return; + } + /** * Resolve as array * @@ -56,7 +96,37 @@ public function resolveType() { // Append resolver if exists if (method_exists($this, 'getResolver')) { - $attributes['resolve'] = [$this, 'getResolver']; + $attributes['resolve'] = function ($root, array $args, $context, ResolveInfo $info) { + // check, if allowed to call this query + if (!$this->authorize($args)) { + throw new Error('UNAUTHORIZED'); + } + + if ($info->returnType instanceof ObjectType) { + $fields = $info->getFieldSelection(GraphQL::FIELD_SELECTION_DEPTH); + } else { + $fields = null; + } + + $opts = [ + 'root' => $root, + 'args' => $args, + 'context' => $context, + 'info' => $info, + 'fields' => $fields, + 'with' => [] + ]; + + // if getSource() returns some model, then guess relation for eager loading + if ($fields !== null && method_exists($this, 'getSource') && is_string($this->getSource())) { + $source = $this->app->make($this->getSource()); + if ($source instanceof Model) { + $opts['with'] = GraphQL::guessWithRelations($source, $fields); + } + } + + return call_user_func_array([$this, 'getResolver'], [$opts]); + }; } return $attributes; diff --git a/src/Support/Pipe/Eloquent/FilterPipe.php b/src/Support/Pipe/Eloquent/FilterPipe.php index 76bc430..d448cc6 100644 --- a/src/Support/Pipe/Eloquent/FilterPipe.php +++ b/src/Support/Pipe/Eloquent/FilterPipe.php @@ -29,9 +29,9 @@ public function handle(Builder $builder, Closure $next, array $opts) { $grammar = null; switch ($driver) { - case 'pgsql' : $grammar = new Grammar\PostgreSQLGrammar ; break; - case 'mysql' : $grammar = new Grammar\MySQLGrammar ; break; - case 'sqlite' : $grammar = new Grammar\SqliteGrammar ; break; + case 'pgsql': $grammar = new Grammar\PostgreSQLGrammar ; break; + case 'mysql': $grammar = new Grammar\MySQLGrammar ; break; + case 'sqlite': $grammar = new Grammar\SqliteGrammar ; break; } // Assert that grammar exists diff --git a/src/Support/Pipe/Pipeline.php b/src/Support/Pipe/Pipeline.php index 8fddb44..760774e 100644 --- a/src/Support/Pipe/Pipeline.php +++ b/src/Support/Pipe/Pipeline.php @@ -18,7 +18,7 @@ class Pipeline extends PipelineBase { * @override * @return \Closure */ - protected function carry() { + protected function carry() { return function ($stack, $pipe) { return function ($passable) use ($stack, $pipe) { if (is_callable($pipe)) { @@ -26,9 +26,7 @@ protected function carry() { // otherwise we'll resolve the pipes out of the container and call it with // the appropriate method and arguments, returning the results back out. return $pipe($passable, $stack, ...$this->parameters); - } - - elseif (!is_object($pipe)) { + } elseif (!is_object($pipe)) { list($name, $parameters) = $this->parsePipeString($pipe); // If the pipe is a string we will parse the string and resolve the class out @@ -36,9 +34,7 @@ protected function carry() { // execute the pipe function giving in the parameters that are required. $pipe = $this->getContainer()->make($name); $parameters = array_merge([$passable, $stack], array_merge($this->parameters, $parameters)); - } - - else { + } else { // If the pipe is already an object we'll just make a callable and pass it to // the pipe as-is. There is no need to do any extra parsing and formatting // since the object we're given was already a fully instantiated object. @@ -48,7 +44,7 @@ protected function carry() { return method_exists($pipe, $this->method) ? $pipe->{$this->method}(...$parameters) : $pipe(...$parameters); }; }; - } + } /** * with diff --git a/src/Support/Transformer/Eloquent/ListTransformer.php b/src/Support/Transformer/Eloquent/ListTransformer.php index a17b379..f3cd32e 100644 --- a/src/Support/Transformer/Eloquent/ListTransformer.php +++ b/src/Support/Transformer/Eloquent/ListTransformer.php @@ -3,6 +3,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; use StudioNet\GraphQL\Support\Transformer\EloquentTransformer; +use StudioNet\GraphQL\Support\Transformer\Paginable; use StudioNet\GraphQL\Support\Definition\Definition; use Illuminate\Database\Eloquent\Builder; use GraphQL\Type\Definition\ObjectType; @@ -13,7 +14,7 @@ * * @see EloquentTransformer */ -class ListTransformer extends EloquentTransformer { +class ListTransformer extends EloquentTransformer implements Paginable { /** * Return query name * diff --git a/src/Support/Transformer/EloquentTransformer.php b/src/Support/Transformer/EloquentTransformer.php index 99205fb..33406df 100644 --- a/src/Support/Transformer/EloquentTransformer.php +++ b/src/Support/Transformer/EloquentTransformer.php @@ -1,9 +1,9 @@ newQuery(); + // this is a small workaround, that should be omitted and reworked with store transformer! + // add with only, if we don't use store transformer + if ((!$opts['transformer'] instanceof StoreTransformer)) { + $query->with($opts['with']); + } return (new Pipeline($this->app)) - ->send($opts['source']->newQuery()) + ->send($query) ->with($opts) ->through($this->getPipes($opts['definition'])) ->then(function (Builder $builder) use ($opts) { diff --git a/src/Support/Transformer/Paginable.php b/src/Support/Transformer/Paginable.php new file mode 100644 index 0000000..edaf659 --- /dev/null +++ b/src/Support/Transformer/Paginable.php @@ -0,0 +1,7 @@ +app; - - return function ($root, array $args, $context, ResolveInfo $info) use ($definition, $app) { - $fields = $info->getFieldSelection(3); - $opts = [ + return function ($root, array $args, $context, ResolveInfo $info) use ($definition) { + // check, if Paginable interface was implemented by given Transformer + // Paginable interface has an extra wrapper for data-fields + $isPaginable = $this instanceof Paginable; + $fieldsDepth = $isPaginable ? GraphQL::FIELD_SELECTION_DEPTH + 1 : GraphQL::FIELD_SELECTION_DEPTH; + $fields = $info->getFieldSelection($fieldsDepth); + $fieldsForGuessingRelations = $isPaginable ? $fields['items'] : $fields; + + $opts = [ 'root' => $root, 'args' => array_filter($args), 'fields' => $fields, 'context' => $context, 'info' => $info, 'transformer' => $this, - 'with' => $this->guessWithRelations($definition, $fields), - 'source' => $app->make($definition->getSource()), + 'with' => GraphQL::guessWithRelations($this->app->make($definition->getSource()), $fieldsForGuessingRelations), + 'source' => $this->app->make($definition->getSource()), 'rules' => $definition->getRules(), 'filterables' => $definition->getFilterable(), 'definition' => $definition, @@ -99,31 +104,6 @@ public function getResolverCallable(Definition $definition) { */ abstract protected function getResolver(array $opts); - /** - * Return relationship based on entity definition construction - * - * @param Definition $definition - * @param array $fields - * @return array - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function guessWithRelations(Definition $definition, array $fields) { - $relations = []; - $source = $this->app->make($definition->getSource()); - - // Parse each field in order to retrieve relationship elements on root - // of array (as relationship are based upon multiple resolvers, we just - // have to handle the root fields here) - foreach ($fields as $key => $field) { - // TODO Improve this checker - if (is_array($field) and method_exists($source, $key)) { - array_push($relations, $key); - } - } - - return $relations; - } - /** * Return availabled arguments * diff --git a/tests/Definition/CamelCaseUserDefinition.php b/tests/Definition/CamelCaseUserDefinition.php index 93b30c1..f9d1ff4 100644 --- a/tests/Definition/CamelCaseUserDefinition.php +++ b/tests/Definition/CamelCaseUserDefinition.php @@ -50,7 +50,11 @@ public function getFetchable() { 'lastLogin' => Type::datetime(), 'isAdmin' => Type::bool(), 'permissions' => Type::json(), - 'posts' => \GraphQL::listOf('post') + 'posts' => \GraphQL::listOf('post'), + 'phone' => \GraphQL::type('phone'), + 'country' => \GraphQL::type('country'), + 'comments' => \GraphQL::listOf('comment'), + 'labels' => \GraphQL::listOf('label') ]; } diff --git a/tests/Definition/CommentDefinition.php b/tests/Definition/CommentDefinition.php new file mode 100644 index 0000000..1ffdea6 --- /dev/null +++ b/tests/Definition/CommentDefinition.php @@ -0,0 +1,77 @@ + Type::id(), + 'body' => Type::string(), +// 'commentable' => \GraphQL::listOf('post') + ]; + } + + /** + * {@inheritDoc} + * + * @return array + */ + public function getMutable() { + return [ + 'id' => Type::id(), + 'body' => Type::string(), + ]; + } + + /** + * {@inheritDoc} + * + * @return array + */ + public function getFilterable() { + return [ + 'id' => new EqualsOrContainsFilter() + ]; + } +} diff --git a/tests/Definition/CountryDefinition.php b/tests/Definition/CountryDefinition.php new file mode 100644 index 0000000..fb67ee7 --- /dev/null +++ b/tests/Definition/CountryDefinition.php @@ -0,0 +1,77 @@ + Type::id(), + 'name' => Type::string(), + 'posts' => \GraphQL::listOf('post') + ]; + } + + /** + * {@inheritDoc} + * + * @return array + */ + public function getMutable() { + return [ + 'id' => Type::id(), + 'name' => Type::string(), + ]; + } + + /** + * {@inheritDoc} + * + * @return array + */ + public function getFilterable() { + return [ + 'id' => new EqualsOrContainsFilter() + ]; + } +} diff --git a/tests/Definition/LabelDefinition.php b/tests/Definition/LabelDefinition.php new file mode 100644 index 0000000..30c562d --- /dev/null +++ b/tests/Definition/LabelDefinition.php @@ -0,0 +1,78 @@ + Type::id(), + 'name' => Type::string(), + 'posts' => \GraphQL::listOf('post'), + 'users' => \GraphQL::listOf('user'), + ]; + } + + /** + * {@inheritDoc} + * + * @return array + */ + public function getMutable() { + return [ + 'id' => Type::id(), + 'name' => Type::string(), + ]; + } + + /** + * {@inheritDoc} + * + * @return array + */ + public function getFilterable() { + return [ + 'id' => new EqualsOrContainsFilter() + ]; + } +} diff --git a/tests/Definition/PhoneDefinition.php b/tests/Definition/PhoneDefinition.php new file mode 100644 index 0000000..5d8f563 --- /dev/null +++ b/tests/Definition/PhoneDefinition.php @@ -0,0 +1,79 @@ + Type::id(), + 'label' => Type::string(), + 'number' => Type::string(), + 'user' => \GraphQL::type('user') + ]; + } + + /** + * {@inheritDoc} + * + * @return array + */ + public function getMutable() { + return [ + 'id' => Type::id(), + 'label' => Type::string(), + 'number' => Type::string(), + ]; + } + + /** + * {@inheritDoc} + * + * @return array + */ + public function getFilterable() { + return [ + 'id' => new EqualsOrContainsFilter() + ]; + } +} diff --git a/tests/Definition/PostDefinition.php b/tests/Definition/PostDefinition.php index 70bcb01..0b14b3c 100644 --- a/tests/Definition/PostDefinition.php +++ b/tests/Definition/PostDefinition.php @@ -48,7 +48,10 @@ public function getFetchable() { 'id' => Type::id(), 'title' => Type::string(), 'content' => Type::string(), + 'author' => \GraphQL::type('user'), 'tags' => \GraphQL::listOf('tag'), + 'comments' => \GraphQL::listOf('comment'), + 'labels' => \GraphQL::listOf('label') ]; } diff --git a/tests/Definition/UserDefinition.php b/tests/Definition/UserDefinition.php index 894a690..81220c8 100644 --- a/tests/Definition/UserDefinition.php +++ b/tests/Definition/UserDefinition.php @@ -52,7 +52,11 @@ public function getFetchable() { 'last_login' => Type::datetime(), 'is_admin' => Type::bool(), 'permissions' => Type::json(), - 'posts' => \GraphQL::listOf('post') + 'posts' => \GraphQL::listOf('post'), + 'phone' => \GraphQL::type('phone'), + 'country' => \GraphQL::type('country'), + 'comments' => \GraphQL::listOf('comment'), + 'labels' => \GraphQL::listOf('label') ]; } diff --git a/tests/Entity/Comment.php b/tests/Entity/Comment.php new file mode 100644 index 0000000..e46c56e --- /dev/null +++ b/tests/Entity/Comment.php @@ -0,0 +1,15 @@ +morphTo(); + } +} diff --git a/tests/Entity/Country.php b/tests/Entity/Country.php new file mode 100644 index 0000000..1eab120 --- /dev/null +++ b/tests/Entity/Country.php @@ -0,0 +1,19 @@ +hasManyThrough(Post::class, User::class); + } +} diff --git a/tests/Entity/Label.php b/tests/Entity/Label.php new file mode 100644 index 0000000..02bafcd --- /dev/null +++ b/tests/Entity/Label.php @@ -0,0 +1,18 @@ +morphedByMany(Post::class, 'labelable'); + } + + public function users() { + return $this->morphedByMany(User::class, 'labelable'); + } +} diff --git a/tests/Entity/Phone.php b/tests/Entity/Phone.php new file mode 100644 index 0000000..eaa6102 --- /dev/null +++ b/tests/Entity/Phone.php @@ -0,0 +1,19 @@ +belongsTo(User::class); + } +} diff --git a/tests/Entity/Post.php b/tests/Entity/Post.php index dcd2b65..77e142a 100644 --- a/tests/Entity/Post.php +++ b/tests/Entity/Post.php @@ -15,16 +15,16 @@ class Post extends Model { /** * Return related posts * - * @return Illuminate\Database\Eloquent\Relations\Relation + * @return \Illuminate\Database\Eloquent\Relations\Relation */ public function author() { - return $this->belongsTo(User::class); + return $this->belongsTo(User::class, 'user_id'); } /** * Return related tags * - * @return Illuminate\Database\Eloquent\Relations\Relation + * @return \Illuminate\Database\Eloquent\Relations\Relation */ public function tags() { return $this->belongsToMany( @@ -34,4 +34,12 @@ public function tags() { 'tag_id' ); } + + public function comments() { + return $this->morphMany(Comment::class, 'commentable'); + } + + public function labels() { + return $this->morphToMany(Label::class, 'labelable'); + } } diff --git a/tests/Entity/Tag.php b/tests/Entity/Tag.php index b23051b..08a7f71 100644 --- a/tests/Entity/Tag.php +++ b/tests/Entity/Tag.php @@ -15,7 +15,7 @@ class Tag extends Model { /** * Return related posts * - * @return Illuminate\Database\Eloquent\Relations\Relation + * @return \Illuminate\Database\Eloquent\Relations\Relation */ public function posts() { return $this->belongsToMany( diff --git a/tests/Entity/User.php b/tests/Entity/User.php index b0fd8f6..ab68560 100644 --- a/tests/Entity/User.php +++ b/tests/Entity/User.php @@ -2,6 +2,8 @@ namespace StudioNet\GraphQL\Tests\Entity; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; /** * User @@ -29,9 +31,30 @@ class User extends Model { /** * Return related posts * - * @return Illuminate\Database\Eloquent\Relations\Relation + * @return HasMany */ public function posts() { return $this->hasMany(Post::class); } + + /** + * Get the phone record associated with the user. + * + * @return HasOne + */ + public function phone() { + return $this->hasOne(Phone::class); + } + + public function country() { + return $this->belongsTo(Country::class); + } + + public function comments() { + return $this->morphMany(Comment::class, 'commentable'); + } + + public function labels() { + return $this->morphToMany(Label::class, 'labelable'); + } } diff --git a/tests/GraphQL/Query/SimpleString.php b/tests/GraphQL/Query/SimpleString.php new file mode 100644 index 0000000..143e22e --- /dev/null +++ b/tests/GraphQL/Query/SimpleString.php @@ -0,0 +1,16 @@ +first(); } } diff --git a/tests/GraphQLMutationTest.php b/tests/GraphQLMutationTest.php index a4007ce..7ac7a29 100644 --- a/tests/GraphQLMutationTest.php +++ b/tests/GraphQLMutationTest.php @@ -1,10 +1,6 @@ create(); - - $graphql = app(GraphQL::class); - $graphql->registerSchema('default', []); - $graphql->registerDefinition(Definition\UserDefinition::class); - $graphql->registerDefinition(Definition\PostDefinition::class); - $graphql->registerDefinition(Definition\TagDefinition::class); + + $this->registerAllDefinitions(); $this->specify('tests mutation on user', function () { $query = 'mutation { user(id: 1, with: { name: "toto" }) { id, name } }'; @@ -102,12 +94,8 @@ public function testMutation() { */ public function testNestedMutation() { factory(Entity\User::class, 5)->create(); - - $graphql = app(GraphQL::class); - $graphql->registerSchema('default', []); - $graphql->registerDefinition(Definition\UserDefinition::class); - $graphql->registerDefinition(Definition\PostDefinition::class); - $graphql->registerDefinition(Definition\TagDefinition::class); + + $this->registerAllDefinitions(); $this->specify('tests nested mutation on user', function () { $query = <<<'GQL' @@ -181,12 +169,8 @@ public function testNestedMutation() { */ public function testNestedEditMutation() { factory(Entity\User::class, 5)->create(); - - $graphql = app(GraphQL::class); - $graphql->registerSchema('default', []); - $graphql->registerDefinition(Definition\UserDefinition::class); - $graphql->registerDefinition(Definition\PostDefinition::class); - $graphql->registerDefinition(Definition\TagDefinition::class); + + $this->registerAllDefinitions(); $this->specify('tests nested mutation on user', function () { $query = <<<'GQL' @@ -256,12 +240,8 @@ public function testNestedEditMutation() { */ public function testNestedEditNullMutation() { factory(Entity\User::class, 5)->create(); - - $graphql = app(GraphQL::class); - $graphql->registerSchema('default', []); - $graphql->registerDefinition(Definition\UserDefinition::class); - $graphql->registerDefinition(Definition\PostDefinition::class); - $graphql->registerDefinition(Definition\TagDefinition::class); + + $this->registerAllDefinitions(); $this->specify('tests nested mutation on user', function () { $query = <<<'GQL' @@ -355,11 +335,7 @@ public function testNestedManyToManyEditMutation() { } $tagsUpdate = implode(",", $tagUpdate); - $graphql = app(GraphQL::class); - $graphql->registerSchema('default', []); - $graphql->registerDefinition(Definition\UserDefinition::class); - $graphql->registerDefinition(Definition\PostDefinition::class); - $graphql->registerDefinition(Definition\TagDefinition::class); + $this->registerAllDefinitions(); $this->specify( 'tests nested m:n mutation on post', @@ -393,12 +369,8 @@ function () use ($post, $tagsToRetrieve, $tagsUpdate) { */ public function testMutationCustomInputField() { factory(Entity\User::class, 1)->create(); - - $graphql = app(GraphQL::class); - $graphql->registerSchema('default', []); - $graphql->registerDefinition(Definition\UserDefinition::class); - $graphql->registerDefinition(Definition\PostDefinition::class); - $graphql->registerDefinition(Definition\TagDefinition::class); + + $this->registerAllDefinitions(); $this->specify('tests custom input field on user', function () { $query = 'mutation { user(id: 1, with: {' @@ -428,12 +400,8 @@ public function testMutationCustomInputField() { */ public function testMutationCustomInputFieldException() { factory(Entity\User::class, 1)->create(); - - $graphql = app(GraphQL::class); - $graphql->registerSchema('default', []); - $graphql->registerDefinition(Definition\UserDefinition::class); - $graphql->registerDefinition(Definition\PostDefinition::class); - $graphql->registerDefinition(Definition\TagDefinition::class); + + $this->registerAllDefinitions(); $this->specify('tests custom input field on user, with error', function () { $query = 'mutation { user(id: 1, with: {' diff --git a/tests/GraphQLPaginationTest.php b/tests/GraphQLPaginationTest.php index 85c7e30..d8237e4 100644 --- a/tests/GraphQLPaginationTest.php +++ b/tests/GraphQLPaginationTest.php @@ -1,10 +1,6 @@ create(['name' => $name]); } - $graphql = app(GraphQL::class); - $graphql->registerSchema('default', []); - $graphql->registerDefinition(Definition\UserDefinition::class); - $graphql->registerDefinition(Definition\PostDefinition::class); - $graphql->registerDefinition(Definition\TagDefinition::class); + $this->registerAllDefinitions(); $this->specify('test order_by', function () { $query = <<<'EOGQL' @@ -52,11 +44,7 @@ public function testOrderByWithPages() { factory(Entity\User::class)->create(['name' => $name]); } - $graphql = app(GraphQL::class); - $graphql->registerSchema('default', []); - $graphql->registerDefinition(Definition\UserDefinition::class); - $graphql->registerDefinition(Definition\PostDefinition::class); - $graphql->registerDefinition(Definition\TagDefinition::class); + $this->registerAllDefinitions(); $query = <<<'EOGQL' query ($skip: Int, $take: Int) { diff --git a/tests/GraphQLRelationsTest.php b/tests/GraphQLRelationsTest.php new file mode 100644 index 0000000..0f40b64 --- /dev/null +++ b/tests/GraphQLRelationsTest.php @@ -0,0 +1,439 @@ +create()->each(function ($user) { + $user->phone()->save(factory(Entity\Phone::class)->make()); + }); + + $this->registerAllDefinitions(); + + // enable query log for comparing queries + DB::enableQueryLog(); + + $this->specify('Testing 1-1 relation', function () { + // fetch data directly + Entity\User::with('phone')->get(); + + // get query log and remove 'time' property from each query-element + $madeQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + DB::flushQueryLog(); + + // fetch data with graphql + $query = 'query { users { items {name phone { label number } }}}'; + $this->executeGraphQL($query); + + // get query log produced duricng fetching over graphql and remove 'time' property from each query-element + $gqlQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + // queries should be the same + $this->assertSame($madeQueries, $gqlQueries); + }); + } + + /** + * Test 1-1 Relation inverse + * + * @return void + */ + public function testOneToOneRelationInverse() { + factory(Entity\User::class, 5)->create()->each(function ($user) { + $user->phone()->save(factory(Entity\Phone::class)->make()); + }); + + $this->registerAllDefinitions(); + + // enable query log for comparing queries + DB::enableQueryLog(); + + $this->specify('Testing 1-1 relation inverse', function () { + // fetch data directly + Entity\Phone::with('user')->get(); + + // get query log and remove 'time' property from each query-element + $madeQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + DB::flushQueryLog(); + + // fetch data with graphql + $query = 'query { phones { items { label number user { name } }}}'; + $this->executeGraphQL($query); + + // get query log produced duricng fetching over graphql and remove 'time' property from each query-element + $gqlQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + // queries should be the same + $this->assertSame($madeQueries, $gqlQueries); + }); + } + + + /** + * Test 1-* Relation + * + * @return void + */ + public function testOneToManyRelation() { + factory(Entity\User::class, 5)->create()->each(function ($user) { + $user->posts()->saveMany(factory(Entity\Post::class, 5)->make()); + }); + + $this->registerAllDefinitions(); + + // enable query log for comparing queries + DB::enableQueryLog(); + + $this->specify('Testing 1-* relation', function () { + // fetch data directly + Entity\User::with('posts')->get(); + + // get query log and remove 'time' property from each query-element + $madeQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + DB::flushQueryLog(); + + // fetch data with graphql + $query = 'query { users { items {name posts { title content } }}}'; + $this->executeGraphQL($query); + + // get query log produced duricng fetching over graphql and remove 'time' property from each query-element + $gqlQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + // queries should be the same + $this->assertSame($madeQueries, $gqlQueries); + }); + } + + + /** + * Test 1-* Relation inverse + * + * @return void + */ + public function testOneToManyRelationInverse() { + factory(Entity\User::class, 5)->create()->each(function ($user) { + $user->posts()->saveMany(factory(Entity\Post::class, 5)->make()); + }); + + $this->registerAllDefinitions(); + + // enable query log for comparing queries + DB::enableQueryLog(); + + $this->specify('Testing 1-* relation inverse', function () { + // fetch data directly + Entity\Post::with('author')->get(); + + // get query log and remove 'time' property from each query-element + $madeQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + DB::flushQueryLog(); + + // fetch data with graphql + $query = 'query { posts { items {title content author { name } }}}'; + $this->executeGraphQL($query); + + // get query log produced duricng fetching over graphql and remove 'time' property from each query-element + $gqlQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + // queries should be the same + $this->assertSame($madeQueries, $gqlQueries); + }); + } + + + /** + * Test HasManyThrough Relation + * + * @return void + */ + public function testHasManyThroughRelation() { + factory(Entity\Country::class, 2)->create()->each(function ($country) { + factory(Entity\User::class, 2)->create([ + 'country_id' => $country->id + ])->each(function ($user) { + factory(Entity\Post::class, 5)->create([ + 'user_id' => $user->id + ]); + }); + }); + + $this->registerAllDefinitions(); + + // enable query log for comparing queries + DB::enableQueryLog(); + + $this->specify('Testing HasManyThrough relation', function () { + // fetch data directly + Entity\Country::with('posts')->get(); + + // get query log and remove 'time' property from each query-element + $madeQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + DB::flushQueryLog(); + + // fetch data with graphql + $query = 'query { countries { items { name posts { title content }}}}'; + $this->executeGraphQL($query); + + // get query log produced duricng fetching over graphql and remove 'time' property from each query-element + $gqlQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + // queries should be the same + $this->assertSame($madeQueries, $gqlQueries); + }); + } + + /** + * Test Polymorphic 1-1 Relation + * + * @return void + */ + public function testPolymorphicOneToOneRelation() { + factory(Entity\User::class, 5)->create()->each(function ($user) { + $user->comments()->saveMany(factory(Entity\Comment::class, 5)->make()); + }); + + $this->registerAllDefinitions(); + + // enable query log for comparing queries + DB::enableQueryLog(); + + $this->specify('Testing polymorphic 1-1 relation', function () { + // fetch data directly + Entity\User::with('comments')->get(); + + // get query log and remove 'time' property from each query-element + $madeQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + DB::flushQueryLog(); + + // fetch data with graphql + $query = 'query { users { items { name comments { body }}}}'; + $this->executeGraphQL($query); + + // get query log produced duricng fetching over graphql and remove 'time' property from each query-element + $gqlQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + // queries should be the same + $this->assertSame($madeQueries, $gqlQueries); + }); + } + + /** + * Test Polymorphic *-* Relation + * + * @return void + */ + public function testPolymorphicManyToManyRelation() { + factory(Entity\User::class, 5)->create()->each(function ($user) { + $user->labels()->saveMany(factory(Entity\Label::class, 5)->make()); + }); + + $this->registerAllDefinitions(); + + // enable query log for comparing queries + DB::enableQueryLog(); + + $this->specify('Testing polymorphic *-* relation', function () { + // fetch data directly + Entity\User::with('labels')->get(); + + // get query log and remove 'time' property from each query-element + $madeQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + DB::flushQueryLog(); + + // fetch data with graphql + $query = 'query { users { items { name labels { name }}}}'; + $this->executeGraphQL($query); + + // get query log produced duricng fetching over graphql and remove 'time' property from each query-element + $gqlQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + // queries should be the same + $this->assertSame($madeQueries, $gqlQueries); + }); + } + + /** + * Test Polymorphic *-* Relation inverse + * + * @return void + */ + public function testPolymorphicManyToManyRelationInverse() { + factory(Entity\User::class, 4)->create()->each(function ($user) { + $user->labels()->saveMany(factory(Entity\Label::class, 3)->make()); + factory(Entity\Post::class, 2)->create([ + 'user_id' => $user->id + ])->each(function ($post) { + $post->labels()->saveMany(factory(Entity\Label::class, 2)->make()); + }); + }); + + $this->registerAllDefinitions(); + + // enable query log for comparing queries + DB::enableQueryLog(); + + $this->specify('Testing polymorphic *-* relation inverse', function () { + // fetch data directly + Entity\Label::with(['users', 'posts'])->get(); + + // get query log and remove 'time' property from each query-element + $madeQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + DB::flushQueryLog(); + + // fetch data with graphql + $query = 'query { labels { items { name users { name } posts { title } }}}'; + $this->executeGraphQL($query); + + // get query log produced duricng fetching over graphql and remove 'time' property from each query-element + $gqlQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + // queries should be the same + $this->assertSame($madeQueries, $gqlQueries); + }); + } + + + /** + * Test deep relations + * + * @return void + */ + public function testDeepRelations() { + factory(Entity\Country::class, 2)->create()->each(function ($country) { + factory(Entity\User::class, 2)->create([ + 'country_id' => $country->id + ])->each(function ($user) { + $user->phone()->save(factory(Entity\Phone::class)->make()); + factory(Entity\Post::class, 2)->create([ + 'user_id' => $user->id + ])->each(function ($post) { + $post->labels()->saveMany(factory(Entity\Label::class, 2)->make()); + $post->tags()->saveMany(factory(Entity\Tag::class, 2)->make()); + }); + }); + }); + + $this->registerAllDefinitions(); + + // enable query log for comparing queries + DB::enableQueryLog(); + + $this->specify('Testing polymorphic *-* relation inverse', function () { + // fetch data directly + Entity\Country::with(['posts', 'posts.labels', 'posts.tags', 'posts.author', 'posts.author.phone'])->get(); + + // get query log and remove 'time' property from each query-element + $madeQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + DB::flushQueryLog(); + + // fetch data with graphql + $query = 'query { + countries { + items { + name + posts { + title + labels { + name + } + tags { + name + } + author { + name + phone { + label + number + } + } + } + } + } + }'; + $this->executeGraphQL($query); + + // get query log produced duricng fetching over graphql and remove 'time' property from each query-element + $gqlQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + // queries should be the same + $this->assertSame($madeQueries, $gqlQueries); + }); + } + + /** + * Test eager loading with custom query + * + * @return void + */ + public function testEagerLoadingWithCustomQuery() { + factory(Entity\User::class, 1)->create()->each(function ($user) { + $user->phone()->save(factory(Entity\Phone::class)->make()); + }); + + $this->registerAllDefinitions([ + 'query' => [ + Viewer::class + ] + ]); + + // enable query log for comparing queries + DB::enableQueryLog(); + + $this->specify('Testing eager loading with custom query', function () { + // fetch data directly + Entity\User::with('phone')->first(); + + // get query log and remove 'time' property from each query-element + $madeQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + DB::flushQueryLog(); + + // fetch data with graphql + $query = 'query { viewer { name phone { label number } }}'; + $this->executeGraphQL($query); + + // get query log produced duricng fetching over graphql and remove 'time' property from each query-element + $gqlQueries = $this->removeTimeElementFromQueryLog(DB::getQueryLog()); + + // queries should be the same + $this->assertSame($madeQueries, $gqlQueries); + }); + } + + + + private function removeTimeElementFromQueryLog(array $queryLog) { + foreach ($queryLog as &$item) { + unset($item['time']); + } + // unset item reference to prevent unexpected stuff + unset($item); + + return $queryLog; + } +} diff --git a/tests/GraphQLTest.php b/tests/GraphQLTest.php index cea366e..591ac83 100644 --- a/tests/GraphQLTest.php +++ b/tests/GraphQLTest.php @@ -3,9 +3,11 @@ use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ObjectType; -use GraphQL\Type\Definition\Type as GraphQLType; use StudioNet\GraphQL\GraphQL; use StudioNet\GraphQL\Tests\Entity; +use StudioNet\GraphQL\Tests\GraphQL\Query\SimpleString; +use StudioNet\GraphQL\Tests\GraphQL\Query\Unauthorized; +use StudioNet\GraphQL\Tests\GraphQL\Query\Viewer; /** * Singleton tests @@ -29,10 +31,9 @@ public function testGetSchemaException() { * @return void */ public function testRegisterDefinition() { + $this->registerAllDefinitions(); + $graphql = app(GraphQL::class); - $graphql->registerDefinition(Definition\UserDefinition::class); - $graphql->registerDefinition(Definition\PostDefinition::class); - $graphql->registerDefinition(Definition\TagDefinition::class); $this->specify('ensure that we can call registered type', function () use ($graphql) { $this->assertInstanceOf(ObjectType::class, $graphql->type('user')); @@ -52,11 +53,7 @@ public function testQuery() { $user->posts()->saveMany(factory(Entity\Post::class, 5)->make()); }); - $graphql = app(GraphQL::class); - $graphql->registerSchema('default', []); - $graphql->registerDefinition(Definition\UserDefinition::class); - $graphql->registerDefinition(Definition\PostDefinition::class); - $graphql->registerDefinition(Definition\TagDefinition::class); + $this->registerAllDefinitions(); $this->specify('test querying a single row', function () { $query = 'query { user(id: 1) { name, posts { title } }}'; @@ -86,11 +83,7 @@ public function testQuery() { public function testScalar() { factory(Entity\User::class)->create(); - $graphql = app(GraphQL::class); - $graphql->registerSchema('default', []); - $graphql->registerDefinition(Definition\UserDefinition::class); - $graphql->registerDefinition(Definition\PostDefinition::class); - $graphql->registerDefinition(Definition\TagDefinition::class); + $this->registerAllDefinitions(); $this->specify('tests datetime rfc3339 type', function () { $query = 'query { user(id: 1) { last_login } }'; @@ -118,15 +111,11 @@ public function testScalar() { public function testCustomQuery() { factory(Entity\User::class)->create(); - $graphql = app(GraphQL::class); - $graphql->registerSchema('default', [ + $this->registerAllDefinitions([ 'query' => [ - \StudioNet\GraphQL\Tests\GraphQL\Query\Viewer::class + Viewer::class ] ]); - $graphql->registerDefinition(Definition\UserDefinition::class); - $graphql->registerDefinition(Definition\PostDefinition::class); - $graphql->registerDefinition(Definition\TagDefinition::class); $this->specify('tests custom query (viewer)', function () { $query = 'query { viewer { id, name }}'; @@ -143,6 +132,67 @@ public function testCustomQuery() { }); } + /** + * Test simple string query + * + * @return void + */ + public function testSimpleStringQuery() { + factory(Entity\User::class)->create(); + + $this->registerAllDefinitions([ + 'query' => [ + SimpleString::class + ] + ]); + + $this->specify('tests simple string query (SimpleString)', function () { + $query = 'query { simplestring }'; + + $this->assertGraphQLEquals($query, [ + 'data' => [ + 'simplestring' => 'You got this!' + ] + ]); + }); + } + + /** + * Test authorize checking + * + * @return void + */ + public function testAuthorizeChecking() { + factory(Entity\User::class)->create(); + + $this->registerAllDefinitions([ + 'query' => [ + Unauthorized::class + ] + ]); + + $this->specify('tests unauthorized query (Unauthorized)', function () { + $query = 'query { unauthorized }'; + + $this->assertGraphQLEquals($query, [ + 'data' => [ + 'unauthorized' => null + ], + 'errors' => [ + [ + 'message' => 'UNAUTHORIZED', + 'locations' => [ + [ + 'line' => 1, + 'column' => 9 + ] + ] + ] + ] + ]); + }); + } + /** * Test camel case query converter * @@ -151,15 +201,11 @@ public function testCustomQuery() { public function testCamelCaseQuery() { factory(Entity\User::class)->create(); - $graphql = app(GraphQL::class); - $graphql->registerSchema('default', [ + $this->registerAllDefinitionsCamelCase([ 'query' => [ - \StudioNet\GraphQL\Tests\GraphQL\Query\Viewer::class + Viewer::class ] ]); - $graphql->registerDefinition(Definition\CamelCaseUserDefinition::class); - $graphql->registerDefinition(Definition\PostDefinition::class); - $graphql->registerDefinition(Definition\TagDefinition::class); $this->specify('test querying a single row with camel case fields', function () { $query = 'query { user(id: 1) { name, isAdmin }}'; @@ -182,11 +228,7 @@ public function testCamelCaseQuery() { public function testFiltersEquality() { factory(Entity\User::class, 2)->create(); - $graphql = app(GraphQL::class); - $graphql->registerSchema('default', []); - $graphql->registerDefinition(Definition\UserDefinition::class); - $graphql->registerDefinition(Definition\PostDefinition::class); - $graphql->registerDefinition(Definition\TagDefinition::class); + $this->registerAllDefinitions(); $this->specify('test equality filtering', function () { $query = <<<'EOGQL' @@ -230,11 +272,7 @@ public function testFiltersEquality() { public function testFiltersContains() { factory(Entity\User::class, 3)->create(); - $graphql = app(GraphQL::class); - $graphql->registerSchema('default', []); - $graphql->registerDefinition(Definition\UserDefinition::class); - $graphql->registerDefinition(Definition\PostDefinition::class); - $graphql->registerDefinition(Definition\TagDefinition::class); + $this->registerAllDefinitions(); $this->specify('test equality containing', function () { $query = <<<'EOGQL' @@ -269,11 +307,7 @@ public function testFiltersCustom() { factory(Entity\User::class)->create(['name' => 'baz']); factory(Entity\User::class)->create(['name' => 'foobar']); - $graphql = app(GraphQL::class); - $graphql->registerSchema('default', []); - $graphql->registerDefinition(Definition\UserDefinition::class); - $graphql->registerDefinition(Definition\PostDefinition::class); - $graphql->registerDefinition(Definition\TagDefinition::class); + $this->registerAllDefinitions(); $this->specify('test equality custom', function () { // We should only get users which name starts with 'ba' diff --git a/tests/TestCase.php b/tests/TestCase.php index a138798..dd32685 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -26,7 +26,7 @@ abstract class TestCase extends BaseTestCase { */ public function setUp() { parent::setUp(); - + // Laravel 5.4 has implemented the service provider's // `loadMigrationsFrom' method and removes the --realpath migrate // option. So, we need to handle unit test for either version @@ -121,4 +121,38 @@ public function executeGraphQL($query, array $opts = []) { public function assertGraphQLEquals($query, array $assert, array $opts = []) { $this->assertSame($assert, $this->executeGraphQL($query, $opts)); } + + /** + * Register all definitions for tests + * + * @param array $registerSchemaOptions Used as second parameter for $graphql->registerSchema() + */ + public function registerAllDefinitions($registerSchemaOptions = []) { + $graphql = app(GraphQL::class); + $graphql->registerSchema('default', $registerSchemaOptions); + $graphql->registerDefinition(Definition\UserDefinition::class); + $graphql->registerDefinition(Definition\PostDefinition::class); + $graphql->registerDefinition(Definition\TagDefinition::class); + $graphql->registerDefinition(Definition\PhoneDefinition::class); + $graphql->registerDefinition(Definition\CountryDefinition::class); + $graphql->registerDefinition(Definition\CommentDefinition::class); + $graphql->registerDefinition(Definition\LabelDefinition::class); + } + + /** + * Register all definitions for tests + * + * @param array $registerSchemaOptions Used as second parameter for $graphql->registerSchema() + */ + public function registerAllDefinitionsCamelCase($registerSchemaOptions = []) { + $graphql = app(GraphQL::class); + $graphql->registerSchema('default', $registerSchemaOptions); + $graphql->registerDefinition(Definition\CamelCaseUserDefinition::class); + $graphql->registerDefinition(Definition\PostDefinition::class); + $graphql->registerDefinition(Definition\TagDefinition::class); + $graphql->registerDefinition(Definition\PhoneDefinition::class); + $graphql->registerDefinition(Definition\CountryDefinition::class); + $graphql->registerDefinition(Definition\CommentDefinition::class); + $graphql->registerDefinition(Definition\LabelDefinition::class); + } }