├── .gitignore ├── README.md ├── assets └── introspection-4.12.txt ├── composer.json ├── config └── config.php ├── docs ├── Configuration.md ├── Overview.md ├── Relay.md └── Schema.md ├── phpunit.xml ├── src ├── Commands │ ├── CacheCommand.php │ ├── FieldMakeCommand.php │ ├── MutationMakeCommand.php │ ├── QueryMakeCommand.php │ ├── SchemaCommand.php │ ├── TypeMakeCommand.php │ └── stubs │ │ ├── eloquent.blade.php │ │ ├── field.stub │ │ ├── mutation.stub │ │ ├── query.stub │ │ └── type.stub ├── Facades │ ├── GraphQL.php │ └── Relay.php ├── Http │ ├── Controllers │ │ ├── LaravelController.php │ │ └── LumenController.php │ └── routes.php ├── LaravelServiceProvider.php ├── LumenServiceProvider.php ├── Node │ ├── NodeQuery.php │ └── NodeType.php ├── Schema │ ├── Connection.php │ ├── Field.php │ ├── FieldCollection.php │ ├── GraphQL.php │ ├── Parser.php │ └── SchemaContainer.php ├── Support │ ├── Cache │ │ └── FileStore.php │ ├── ConnectionResolver.php │ ├── Definition │ │ ├── EdgeType.php │ │ ├── EloquentType.php │ │ ├── GraphQLField.php │ │ ├── GraphQLInterface.php │ │ ├── GraphQLMutation.php │ │ ├── GraphQLQuery.php │ │ ├── GraphQLType.php │ │ ├── PageInfoType.php │ │ ├── RelayConnectionType.php │ │ ├── RelayMutation.php │ │ └── RelayType.php │ ├── SchemaGenerator.php │ └── ValidationError.php └── Traits │ ├── GlobalIdTrait.php │ ├── MutationTestTrait.php │ ├── RelayMiddleware.php │ └── RelayModelTrait.php └── tests ├── BaseTest.php ├── IntrospectionTest.php ├── ObjectIdentificationTest.php └── assets ├── Data └── StarWarsData.php ├── Queries ├── HumanByName.php └── UpdateHeroNameQuery.php └── Types └── HumanType.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | composer.lock 3 | vendor 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This project is deprecated in favor of https://github.com/nuwave/lighthouse 2 | 3 | # laravel-grapql-relay 4 | 5 | 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). 6 | 7 | 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. 8 | 9 | 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! 10 | 11 | ### Installation ### 12 | 13 | You must then modify your composer.json file and run composer update to include the latest version of the package in your project. 14 | 15 | ```php 16 | "require": { 17 | "nuwave/laravel-graphql-relay": "0.3.*" 18 | } 19 | ``` 20 | 21 | Or you can use the composer require command from your terminal. 22 | 23 | ```bash 24 | composer require nuwave/laravel-graphql-relay 25 | ``` 26 | 27 | Add the service provider to your ```config/app.php``` file 28 | 29 | ```php 30 | Nuwave\Relay\LaravelServiceProvider::class 31 | ``` 32 | 33 | Add the Relay & GraphQL facade to your app/config.php file 34 | 35 | ```php 36 | 'GraphQL' => Nuwave\Relay\Facades\GraphQL::class, 37 | 'Relay' => Nuwave\Relay\Facades\Relay::class, 38 | ``` 39 | 40 | Publish the configuration file 41 | 42 | ```bash 43 | php artisan vendor:publish --provider="Nuwave\Relay\LaravelServiceProvider" 44 | ``` 45 | 46 | Create a ```schema.php``` file and add the path to the config. See the [Schema](https://github.com/nuwave/laravel-graphql-relay/wiki/3.-Schema#schema-file) wiki page for more information on `schema.php`. 47 | 48 | ```php 49 | // config/relay.php 50 | // ... 51 | 'schema' => [ 52 | 'path' => 'Http/schema.php', 53 | 'output' => null, 54 | ], 55 | ``` 56 | 57 | To generate a ```schema.json``` file (used with the Babel Relay Plugin): 58 | 59 | ```bash 60 | php artisan relay:schema 61 | ``` 62 | 63 | *You can customize the output path in the ```relay.php``` config file under ```schema.output```* 64 | 65 | For additional documentation, look through the docs folder or read the Wiki. 66 | -------------------------------------------------------------------------------- /assets/introspection-4.12.txt: -------------------------------------------------------------------------------- 1 | query IntrospectionQuery { 2 | __schema { 3 | queryType { name } 4 | mutationType { name } 5 | # Currently this package does not support subscriptions... 6 | # but maybe in the future! 7 | # subscriptionType { name } 8 | types { 9 | ...FullType 10 | } 11 | directives { 12 | name 13 | description 14 | args { 15 | ...InputValue 16 | } 17 | onOperation 18 | onFragment 19 | onField 20 | } 21 | } 22 | } 23 | fragment FullType on __Type { 24 | kind 25 | name 26 | description 27 | fields(includeDeprecated: true) { 28 | name 29 | description 30 | args { 31 | ...InputValue 32 | } 33 | type { 34 | ...TypeRef 35 | } 36 | isDeprecated 37 | deprecationReason 38 | } 39 | inputFields { 40 | ...InputValue 41 | } 42 | interfaces { 43 | ...TypeRef 44 | } 45 | enumValues(includeDeprecated: true) { 46 | name 47 | description 48 | isDeprecated 49 | deprecationReason 50 | } 51 | possibleTypes { 52 | ...TypeRef 53 | } 54 | } 55 | fragment InputValue on __InputValue { 56 | name 57 | description 58 | type { ...TypeRef } 59 | defaultValue 60 | } 61 | fragment TypeRef on __Type { 62 | kind 63 | name 64 | ofType { 65 | kind 66 | name 67 | ofType { 68 | kind 69 | name 70 | ofType { 71 | kind 72 | name 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuwave/laravel-graphql-relay", 3 | "description": "Adds relay specifications to laravel graphql server", 4 | "keywords": ["laravel", "relay", "graphql", "react"], 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Christopher Moore", 10 | "email": "chris@nuwavecommerce.com" 11 | } 12 | ], 13 | "autoload": { 14 | "psr-4": { 15 | "Nuwave\\Relay\\": "src/" 16 | } 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "4.*", 20 | "orchestra/testbench": "~3.0" 21 | }, 22 | "autoload-dev": { 23 | "psr-4": { 24 | "Nuwave\\Relay\\Tests\\": "tests/" 25 | } 26 | }, 27 | "require": { 28 | "webonyx/graphql-php": "~0.5", 29 | "illuminate/console": "5.*", 30 | "doctrine/dbal": "^2.5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'mutations' => 'App\\GraphQL\\Mutations', 18 | 'queries' => 'App\\GraphQL\\Queries', 19 | 'types' => 'App\\GraphQL\\Types', 20 | 'fields' => 'App\\GraphQL\\Fields', 21 | ], 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Schema declaration 26 | |-------------------------------------------------------------------------- 27 | | 28 | | This is a path that points to where your Relay schema is located 29 | | relative to the app path. You should define your entire Relay 30 | | schema in this file. Declare any Relay queries, mutations, 31 | | and types here instead of laravel-graphql config file. 32 | | 33 | */ 34 | 35 | 'schema' => [ 36 | 'path' => null, 37 | 'output' => null, 38 | ], 39 | 40 | 'controller' => 'Nuwave\Relay\Http\Controllers\LaravelController@query', 41 | 'model_path' => 'App\\Models', 42 | 'camel_case' => false, 43 | ]; 44 | -------------------------------------------------------------------------------- /docs/Configuration.md: -------------------------------------------------------------------------------- 1 | ## Configuration ## 2 | 3 | When publishing the configuration, the package will create a ```relay.php``` file in your ```config``` folder. 4 | 5 | ### Namespaces ### 6 | 7 | ```php 8 | 'namespaces' => [ 9 | 'mutations' => 'App\\GraphQL\\Mutations', 10 | 'queries' => 'App\\GraphQL\\Queries', 11 | 'types' => 'App\\GraphQL\\Types', 12 | 'fields' => 'App\\GraphQL\\Fields', 13 | ], 14 | ``` 15 | 16 | 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. 17 | 18 | ### Schema ### 19 | 20 | ```php 21 | 'schema' => [ 22 | 'file' => 'Http/GraphQL/schema.php', 23 | 'output' => null, 24 | ] 25 | ``` 26 | 27 | ** File ** 28 | 29 | 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) 30 | 31 | ** Output ** 32 | 33 | 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)). 34 | 35 | ### Eloquent ### 36 | 37 | ```php 38 | 'eloquent' => [ 39 | 'path' => 'App\\Models', 40 | 'camel_case' => false 41 | ] 42 | ``` 43 | 44 | ** Path ** 45 | 46 | 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). 47 | 48 | ** Camel Case ** 49 | 50 | 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. 51 | 52 | *This works great with the [Eloquence package](https://github.com/kirkbushell/eloquence).* 53 | -------------------------------------------------------------------------------- /docs/Overview.md: -------------------------------------------------------------------------------- 1 | # laravel-grapql-relay # 2 | 3 | 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). 4 | 5 | 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. 6 | 7 | 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! 8 | 9 | ### Installation ### 10 | 11 | You must then modify your composer.json file and run composer update to include the latest version of the package in your project. 12 | 13 | ```php 14 | "require": { 15 | "nuwave/laravel-graphql-relay": "0.3.*" 16 | } 17 | ``` 18 | 19 | Or you can use the composer require command from your terminal. 20 | 21 | ``` 22 | composer require nuwave/laravel-graphql-relay 23 | ``` 24 | 25 | Add the service provider to your ```app/config.php``` file 26 | 27 | ``` 28 | Nuwave\Relay\LaravelServiceProvider::class 29 | ``` 30 | 31 | Add the Relay & GraphQL facade to your app/config.php file 32 | 33 | ``` 34 | 'GraphQL' => Nuwave\Relay\Facades\GraphQL::class, 35 | 'Relay' => Nuwave\Relay\Facades\Relay::class, 36 | ``` 37 | 38 | Publish the configuration file 39 | 40 | ``` 41 | php artisan vendor:publish --provider="Nuwave\Relay\LaravelServiceProvider" 42 | ``` 43 | 44 | Create a ```schema.php``` file and add the path to the config 45 | 46 | ``` 47 | // config/relay.php 48 | // ... 49 | 'schema' => [ 50 | 'path' => 'Http/schema.php', 51 | 'output' => null, 52 | ], 53 | ``` 54 | 55 | To generate a ```schema.json``` file (used with the Babel Relay Plugin): 56 | 57 | ``` 58 | php artisan relay:schema 59 | ``` 60 | 61 | *You can customize the output path in the ```relay.php``` config file under ```schema.output```* 62 | 63 | For additional documentation, look through the docs folder or read the Wiki. 64 | -------------------------------------------------------------------------------- /docs/Relay.md: -------------------------------------------------------------------------------- 1 | ## Object Identification 2 | 3 | Facebook Relay [Documentation](https://facebook.github.io/relay/docs/graphql-object-identification.html#content) 4 | 5 | Facebook GraphQL [Spec](https://facebook.github.io/relay/graphql/objectidentification.htm) 6 | 7 | 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. 8 | 9 | Example: 10 | 11 | ```php 12 | 'Customer', 30 | 'description' => 'A Customer model.', 31 | ]; 32 | 33 | /** 34 | * Get customer by id. 35 | * 36 | * When the root 'node' query is called, it will use this method 37 | * to resolve the type by providing the id. 38 | * 39 | * @param string $id 40 | * @return Customer 41 | */ 42 | public function resolveById($id) 43 | { 44 | return Customer::find($id); 45 | } 46 | 47 | /** 48 | * Available fields of Type. 49 | * 50 | * @return array 51 | */ 52 | public function relayFields() 53 | { 54 | return [ 55 | // Note: You may omit the id field as it will be overwritten to adhere to 56 | // the NodeInterface 57 | 'id' => [ 58 | 'type' => Type::nonNull(Type::id()), 59 | 'description' => 'ID of the customer.' 60 | ], 61 | // ... 62 | ]; 63 | } 64 | } 65 | ``` 66 | 67 | ## Connections 68 | 69 | Facebook Relay [Documentation](https://facebook.github.io/relay/docs/graphql-connections.html#content) 70 | 71 | Facebook GraphQL [Spec](https://facebook.github.io/relay/graphql/connections.htm) 72 | 73 | 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. 74 | 75 | *(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.)* 76 | 77 | Example: 78 | 79 | ```php 80 | GraphQL::connection('order', function ($customer, array $args, ResolveInfo $info) { 98 | // Note: This is just an example. This type of resolve functionality may not make sense for your 99 | // application so just use what works best for you. However, you will need to pass back an object 100 | // that implements the LengthAwarePaginator as mentioned above in order for it to work with the 101 | // Relay connection spec. 102 | $orders = $customer->orders; 103 | 104 | if (isset($args['first'])) { 105 | $total = $orders->count(); 106 | $first = $args['first']; 107 | $after = $this->decodeCursor($args); 108 | $currentPage = $first && $after ? floor(($first + $after) / $first) : 1; 109 | 110 | return new Paginator( 111 | $orders->slice($after)->take($first), 112 | $total, 113 | $first, 114 | $currentPage 115 | ); 116 | } 117 | 118 | return new Paginator( 119 | $orders, 120 | $orders->count(), 121 | $orders->count() 122 | ); 123 | }), 124 | // Alternatively, you can let the package resolve this connection for you 125 | // by passing the name of the relationship. 126 | 'orders' => GraphQL::connection('order', 'orders') 127 | ]; 128 | } 129 | } 130 | ``` 131 | -------------------------------------------------------------------------------- /docs/Schema.md: -------------------------------------------------------------------------------- 1 | ## Schema 2 | 3 | ### Types 4 | 5 | Creating a Type: 6 | 7 | ``` 8 | php artisan make:relay:type UserType 9 | ``` 10 | 11 | ```php 12 | 'User', 28 | 'description' => 'A user of the application.', 29 | ]; 30 | 31 | /** 32 | * Get user by id. 33 | * 34 | * @param string $id 35 | * @return User 36 | */ 37 | public function resolveById($id) 38 | { 39 | return User::find($id); 40 | } 41 | 42 | /** 43 | * Available fields of Type. 44 | * 45 | * @return array 46 | */ 47 | public function relayFields() 48 | { 49 | return [ 50 | 'id' => [ 51 | 'type' => Type::nonNull(Type::id()), 52 | 'description' => 'The primary id of the user.' 53 | ], 54 | 'name' => [ 55 | 'type' => Type::string(), 56 | 'description' => 'Full name of user.' 57 | ], 58 | 'email' => [ 59 | 'type' => Type::string(), 60 | 'description' => 'Email address of user.' 61 | ] 62 | // ... 63 | ] 64 | } 65 | } 66 | ``` 67 | 68 | ### Queries 69 | 70 | Create a Query: 71 | 72 | ```bash 73 | php artisan make:relay:query UserQuery 74 | ``` 75 | 76 | ```php 77 | [ 107 | 'type' => Type::nonNull(Type::string()), 108 | ] 109 | ]; 110 | } 111 | 112 | /** 113 | * Resolve the query. 114 | * 115 | * @param mixed $root 116 | * @param array $args 117 | * @return mixed 118 | */ 119 | public function resolve($root, array $args) 120 | { 121 | return User::find($args['id']); 122 | } 123 | } 124 | 125 | ``` 126 | 127 | ### Mutations 128 | 129 | Create a mutation: 130 | 131 | ```bash 132 | php artisan make:relay:mutation 133 | ``` 134 | 135 | ```php 136 | [ 168 | 'type' => Type::string(), 169 | 'rules' => ['required'] 170 | ], 171 | 'password' => [ 172 | 'type' => Type::string() 173 | ] 174 | ]; 175 | } 176 | 177 | /** 178 | * Rules for mutation. 179 | * 180 | * Note: You can add your rules here or define 181 | * them in the inputFields 182 | * 183 | * @return array 184 | */ 185 | public function rules() 186 | { 187 | return [ 188 | 'password' => ['required', 'min:15'] 189 | ]; 190 | } 191 | 192 | /** 193 | * Fields that will be sent back to client. 194 | * 195 | * @return array 196 | */ 197 | protected function outputFields() 198 | { 199 | return [ 200 | 'user' => [ 201 | 'type' => GraphQL::type('user'), 202 | 'resolve' => function (User $user) { 203 | return $user; 204 | } 205 | ] 206 | ]; 207 | } 208 | 209 | /** 210 | * Perform data mutation. 211 | * 212 | * @param array $input 213 | * @param ResolveInfo $info 214 | * @return array 215 | */ 216 | protected function mutateAndGetPayload(array $input, ResolveInfo $info) 217 | { 218 | $user = User::find($input['id']); 219 | $user->password = \Hash::make($input['password']); 220 | $user->save(); 221 | 222 | return $user; 223 | } 224 | } 225 | 226 | ``` 227 | 228 | ### Custom Fields 229 | 230 | Create a custom field: 231 | 232 | ```bash 233 | php artisan relay:make:field Avatar 234 | ``` 235 | 236 | ```php 237 | 'Avatar of user.' 255 | ]; 256 | 257 | /** 258 | * The return type of the field. 259 | * 260 | * @return Type 261 | */ 262 | public function type() 263 | { 264 | return Type::string(); 265 | } 266 | 267 | /** 268 | * Available field arguments. 269 | * 270 | * @return array 271 | */ 272 | public function args() 273 | { 274 | return [ 275 | 'width' => [ 276 | 'type' => Type::int(), 277 | 'description' => 'The width of the picture' 278 | ], 279 | 'height' => [ 280 | 'type' => Type::int(), 281 | 'description' => 'The height of the picture' 282 | ] 283 | ]; 284 | } 285 | 286 | /** 287 | * Resolve the field. 288 | * 289 | * @param mixed $root 290 | * @param array $args 291 | * @return mixed 292 | */ 293 | public function resolve($root, array $args) 294 | { 295 | $width = isset($args['width']) ? $args['width'] : 100; 296 | $height = isset($args['height']) ? $args['height'] : 100; 297 | 298 | return 'http://placehold.it/'.$root->id.'/'.$width.'x'.$height; 299 | } 300 | } 301 | ``` 302 | 303 | **To use the field in your Type(s):** 304 | 305 | ```php 306 | Avatar::field() 318 | ] 319 | } 320 | 321 | ``` 322 | 323 | ### Schema File 324 | 325 | 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. 326 | 327 | *Be sure your file name is located in the ```relay.php``` config file* 328 | 329 | ```php 330 | // config/relay.php 331 | 332 | 'schema' => [ 333 | 'path' => 'Http/schema.php', 334 | 'output' => null 335 | ], 336 | ``` 337 | 338 | ```php 339 | // app/Http/schema.php 340 | 341 | Relay::group(['namespace' => 'App\\Http\\GraphQL', 'middleware' => 'auth'], function () { 342 | Relay::group(['namespace' => 'Mutations'], function () { 343 | Relay::mutation('createUser', 'CreateUserMutation'); 344 | }); 345 | 346 | Relay::group(['namespace' => 'Queries'], function () { 347 | Relay::query('userQuery', 'UserQuery'); 348 | }); 349 | 350 | Relay::group(['namespace' => 'Types'], function () { 351 | Relay::type('user', 'UserType'); 352 | }); 353 | }); 354 | ``` 355 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | ./tests/ 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Commands/CacheCommand.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 42 | } 43 | 44 | /** 45 | * Execute the console command. 46 | * 47 | * @return mixed 48 | */ 49 | public function handle() 50 | { 51 | $this->cache->flush(); 52 | 53 | app('graphql')->schema(); 54 | 55 | $this->info('Eloquent Types successfully cached.'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Commands/FieldMakeCommand.php: -------------------------------------------------------------------------------- 1 | generator = $generator; 39 | 40 | parent::__construct(); 41 | } 42 | 43 | /** 44 | * Execute the console command. 45 | * 46 | * @return mixed 47 | */ 48 | public function handle() 49 | { 50 | $data = $this->generator->execute(); 51 | 52 | if (!isset($data['data']['__schema'])) { 53 | $this->error('There was an error when attempting to generate the schema file.'); 54 | $this->line(json_encode($data)); 55 | } 56 | 57 | $this->info('Schema file successfully generated.'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Commands/TypeMakeCommand.php: -------------------------------------------------------------------------------- 1 | option('model')) { 63 | $this->setViewPath(); 64 | $stub = $this->getEloquentStub($model); 65 | } else { 66 | $stub = $this->files->get($this->getStub()); 67 | } 68 | 69 | return $this->replaceNamespace($stub, $name)->replaceClass($stub, $name); 70 | } 71 | 72 | /** 73 | * Get the console command options. 74 | * 75 | * @return array 76 | */ 77 | protected function getOptions() 78 | { 79 | return [ 80 | ['model', null, InputOption::VALUE_OPTIONAL, 'Generate a Eloquent GraphQL type.'], 81 | ]; 82 | } 83 | 84 | /** 85 | * Set config view paths. 86 | * 87 | * @return void 88 | */ 89 | protected function setViewPath() 90 | { 91 | $paths = config('view.paths'); 92 | $paths[] = realpath(__DIR__.'/stubs'); 93 | 94 | config(['view.paths' => $paths]); 95 | } 96 | 97 | /** 98 | * Generate stub from eloquent type. 99 | * 100 | * @param string $model 101 | * @return string 102 | */ 103 | protected function getEloquentStub($model) 104 | { 105 | $shortName = $model; 106 | $rootNamespace = $this->laravel->getNamespace(); 107 | 108 | if (starts_with($model, $rootNamespace)) { 109 | $shortName = (new ReflectionClass($model))->getShortName(); 110 | } else { 111 | $model = config('relay.model_path') . "\\" . $model; 112 | } 113 | 114 | $fields = $this->getTypeFields($model); 115 | 116 | return "render(); 117 | } 118 | 119 | /** 120 | * Generate fields for type. 121 | * 122 | * @param string $class 123 | * @return array 124 | */ 125 | protected function getTypeFields($class) 126 | { 127 | $model = app($class); 128 | 129 | return (new EloquentType($model))->rawFields(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Commands/stubs/eloquent.blade.php: -------------------------------------------------------------------------------- 1 | namespace DummyNamespace; 2 | 3 | use GraphQL; 4 | use GraphQL\Type\Definition\Type; 5 | use Nuwave\Relay\Support\Definition\RelayType; 6 | use GraphQL\Type\Definition\ResolveInfo; 7 | use {{ $model }}; 8 | 9 | class DummyClass extends RelayType 10 | { 11 | /** 12 | * Attributes of Type. 13 | * 14 | * @var array 15 | */ 16 | protected $attributes = [ 17 | 'name' => '{{ $shortName }}', 18 | 'description' => '', 19 | ]; 20 | 21 | /** 22 | * Get customer by id. 23 | * 24 | * When the root 'node' query is called, it will use this method 25 | * to resolve the type by providing the id. 26 | * 27 | * @param string $id 28 | * @return User 29 | */ 30 | public function resolveById($id) 31 | { 32 | return {{ $shortName }}::findOrFail($id); 33 | } 34 | 35 | /** 36 | * Available fields of Type. 37 | * 38 | * @return array 39 | */ 40 | public function relayFields() 41 | { 42 | return [ 43 | @foreach($fields as $key => $field) 44 | '{{ $key }}' => [ 45 | 'type' => {{ $field['type'] }}, 46 | 'description' => '{{ $field['description'] }}', 47 | ], 48 | @endforeach 49 | ]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Commands/stubs/field.stub: -------------------------------------------------------------------------------- 1 | '' 19 | ]; 20 | 21 | /** 22 | * The return type of the field. 23 | * 24 | * @return Type 25 | */ 26 | public function type() 27 | { 28 | // return Type::string(); 29 | } 30 | 31 | /** 32 | * Available field arguments. 33 | * 34 | * @return array 35 | */ 36 | public function args() 37 | { 38 | return []; 39 | } 40 | 41 | /** 42 | * Resolve the field. 43 | * 44 | * @param mixed $root 45 | * @param array $args 46 | * @return mixed 47 | */ 48 | public function resolve($root, array $args) 49 | { 50 | // TODO: Resolve field 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Commands/stubs/mutation.stub: -------------------------------------------------------------------------------- 1 | '', 19 | 'description' => '', 20 | ]; 21 | 22 | /** 23 | * Get model by id. 24 | * 25 | * When the root 'node' query is called, it will use this method 26 | * to resolve the type by providing the id. 27 | * 28 | * @param string $id 29 | * @return \Eloquence\Database\Model 30 | */ 31 | public function resolveById($id) 32 | { 33 | // return Model::find($id); 34 | } 35 | 36 | /** 37 | * Available fields of Type. 38 | * 39 | * @return array 40 | */ 41 | public function relayFields() 42 | { 43 | return []; 44 | } 45 | 46 | /** 47 | * List of related connections. 48 | * 49 | * @return array 50 | */ 51 | public function connections() 52 | { 53 | return []; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Facades/GraphQL.php: -------------------------------------------------------------------------------- 1 | setupQuery($request); 21 | } 22 | 23 | /** 24 | * Execute GraphQL query. 25 | * 26 | * @param Request $request 27 | * @return Response 28 | */ 29 | public function query(Request $request) 30 | { 31 | $query = $request->get('query'); 32 | $params = $request->get('variables'); 33 | 34 | if (is_string($params)) { 35 | $params = json_decode($params, true); 36 | } 37 | 38 | return app('graphql')->query($query, $params); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Http/Controllers/LumenController.php: -------------------------------------------------------------------------------- 1 | get('query'); 19 | 20 | $params = $request->get('variables'); 21 | 22 | if (is_string($params)) { 23 | $params = json_decode($params, true); 24 | } 25 | 26 | return app('graphql')->query($query, $params); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Http/routes.php: -------------------------------------------------------------------------------- 1 | 'graphql', 'uses' => $controller]); 6 | -------------------------------------------------------------------------------- /src/LaravelServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([__DIR__ . '/../config/config.php' => config_path('relay.php')]); 26 | 27 | $this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'relay'); 28 | 29 | $this->registerSchema(); 30 | 31 | if (config('relay.controller')) { 32 | include __DIR__.'/Http/routes.php'; 33 | } 34 | } 35 | 36 | /** 37 | * Register any application services. 38 | * 39 | * @return void 40 | */ 41 | public function register() 42 | { 43 | $this->commands([ 44 | SchemaCommand::class, 45 | CacheCommand::class, 46 | MutationMakeCommand::class, 47 | FieldMakeCommand::class, 48 | QueryMakeCommand::class, 49 | TypeMakeCommand::class, 50 | ]); 51 | 52 | $this->app->singleton('graphql', function ($app) { 53 | return new GraphQL($app); 54 | }); 55 | 56 | $this->app->singleton('relay', function ($app) { 57 | return new SchemaContainer(new Parser); 58 | }); 59 | } 60 | 61 | /** 62 | * Register schema mutations and queries. 63 | * 64 | * @return void 65 | */ 66 | protected function registerSchema() 67 | { 68 | if (config('relay.schema.path')) { 69 | require_once app_path(config('relay.schema.path')); 70 | } 71 | 72 | $this->registerRelayTypes(); 73 | 74 | $this->setGraphQLConfig(); 75 | 76 | $this->initializeTypes(); 77 | } 78 | 79 | /** 80 | * Register the default relay types in the schema. 81 | * 82 | * @return void 83 | */ 84 | protected function registerRelayTypes() 85 | { 86 | $relay = $this->app['relay']; 87 | 88 | $relay->group(['namespace' => 'Nuwave\\Relay'], function () use ($relay) { 89 | $relay->query('node', 'Node\\NodeQuery'); 90 | $relay->type('node', 'Node\\NodeType'); 91 | $relay->type('pageInfo', 'Support\\Definition\\PageInfoType'); 92 | }); 93 | } 94 | 95 | /** 96 | * Set GraphQL configuration variables. 97 | * 98 | * @return void 99 | */ 100 | protected function setGraphQLConfig() 101 | { 102 | $relay = $this->app['relay']; 103 | 104 | $mutations = config('relay.schema.mutations', []); 105 | $queries = config('relay.schema.queries', []); 106 | $types = config('relay.schema.types', []); 107 | 108 | config([ 109 | 'relay.schema.mutations' => array_merge($mutations, $relay->getMutations()->config()), 110 | 'relay.schema.queries' => array_merge($queries, $relay->getQueries()->config()), 111 | 'relay.schema.types' => array_merge($types, $relay->getTypes()->config()) 112 | ]); 113 | } 114 | 115 | /** 116 | * Initialize GraphQL types array. 117 | * 118 | * @return void 119 | */ 120 | protected function initializeTypes() 121 | { 122 | foreach (config('relay.schema.types') as $name => $type) { 123 | $this->app['graphql']->addType($type, $name); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/LumenServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__ . '/../config/config.php', 'relay'); 26 | 27 | $this->registerSchema(); 28 | } 29 | 30 | /** 31 | * Register any application services. 32 | * 33 | * @return void 34 | */ 35 | public function register() 36 | { 37 | $this->commands([ 38 | SchemaCommand::class, 39 | CacheCommand::class, 40 | MutationMakeCommand::class, 41 | FieldMakeCommand::class, 42 | QueryMakeCommand::class, 43 | TypeMakeCommand::class, 44 | ]); 45 | 46 | $this->app->singleton('graphql', function ($app) { 47 | return new GraphQL($app); 48 | }); 49 | 50 | $this->app->singleton('relay', function ($app) { 51 | return new SchemaContainer(new Parser); 52 | }); 53 | } 54 | 55 | /** 56 | * Register schema mutations and queries. 57 | * 58 | * @return void 59 | */ 60 | protected function registerSchema() 61 | { 62 | $this->registerRelayTypes(); 63 | 64 | if (config('relay.schema.path')) { 65 | require_once __DIR__ . '/../../../../app/' . config('relay.schema.path'); 66 | } 67 | 68 | $this->setGraphQLConfig(); 69 | 70 | $this->initializeTypes(); 71 | } 72 | 73 | /** 74 | * Register the default relay types in the schema. 75 | * 76 | * @return void 77 | */ 78 | protected function registerRelayTypes() 79 | { 80 | $relay = $this->app['relay']; 81 | 82 | $relay->group(['namespace' => 'Nuwave\\Relay'], function () use ($relay) { 83 | $relay->query('node', 'Node\\NodeQuery'); 84 | $relay->type('node', 'Node\\NodeType'); 85 | $relay->type('pageInfo', 'Support\\Definition\\PageInfoType'); 86 | }); 87 | } 88 | 89 | /** 90 | * Set GraphQL configuration variables. 91 | * 92 | * @return void 93 | */ 94 | protected function setGraphQLConfig() 95 | { 96 | $relay = $this->app['relay']; 97 | 98 | $mutations = config('relay.schema.mutations', []); 99 | $queries = config('relay.schema.queries', []); 100 | $types = config('relay.schema.types', []); 101 | 102 | config([ 103 | 'relay.schema.mutations' => array_merge($mutations, $relay->getMutations()->config()), 104 | 'relay.schema.queries' => array_merge($queries, $relay->getQueries()->config()), 105 | 'relay.schema.types' => array_merge($types, $relay->getTypes()->config()) 106 | ]); 107 | } 108 | 109 | /** 110 | * Initialize GraphQL types array. 111 | * 112 | * @return void 113 | */ 114 | protected function initializeTypes() 115 | { 116 | foreach (config('relay.schema.types') as $name => $type) { 117 | $this->app['graphql']->addType($type, $name); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Node/NodeQuery.php: -------------------------------------------------------------------------------- 1 | [ 34 | 'name' => 'id', 35 | 'type' => Type::nonNull(Type::id()) 36 | ] 37 | ]; 38 | } 39 | 40 | /** 41 | * Resolve query. 42 | * 43 | * @param string $root 44 | * @param array $args 45 | * @return Illuminate\Database\Eloquent\Model|array 46 | */ 47 | public function resolve($root, array $args, ResolveInfo $info) 48 | { 49 | // Here, we decode the base64 id and get the id of the type 50 | // as well as the type's name. 51 | list($typeClass, $id) = $this->decodeGlobalId($args['id']); 52 | 53 | foreach (config('relay.schema.types') as $type => $class) { 54 | if ($typeClass == $class) { 55 | $objectType = app($typeClass); 56 | 57 | $model = $objectType->resolveById($id); 58 | 59 | if (is_array($model)) { 60 | $model['graphqlType'] = $type; 61 | } elseif (is_object($model)) { 62 | $model->graphqlType = $type; 63 | } 64 | 65 | return $model; 66 | } 67 | } 68 | 69 | return null; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Node/NodeType.php: -------------------------------------------------------------------------------- 1 | 'Node', 18 | 'description' => 'An object with an ID.' 19 | ]; 20 | 21 | /** 22 | * Available fields on type. 23 | * 24 | * @return array 25 | */ 26 | public function fields() 27 | { 28 | return [ 29 | 'id' => [ 30 | 'type' => Type::nonNull(Type::id()), 31 | 'description' => 'The id of the object.' 32 | ] 33 | ]; 34 | } 35 | 36 | /** 37 | * Resolve the interface. 38 | * 39 | * @param mixed $obj 40 | * @return mixed 41 | */ 42 | public function resolveType($obj) 43 | { 44 | if (is_array($obj)) { 45 | return GraphQL::type($obj['graphqlType']); 46 | } 47 | 48 | return GraphQL::type($obj->graphqlType); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Schema/Connection.php: -------------------------------------------------------------------------------- 1 | arguments); 52 | } 53 | 54 | /** 55 | * Set arguments of selection. 56 | * 57 | * @param ASTField $field 58 | */ 59 | public function setArguments(ASTField $field) 60 | { 61 | if ($field->arguments) { 62 | foreach ($field->arguments as $argument) { 63 | $this->arguments[$argument->name->value] = $argument->value->value; 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * Set connection path. 70 | * 71 | * @param string $path 72 | */ 73 | public function setPath($path = '') 74 | { 75 | $this->path = $path; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Schema/Field.php: -------------------------------------------------------------------------------- 1 | name = $name; 40 | $this->namespace = $namespace; 41 | } 42 | 43 | /** 44 | * Attach middleware to field. 45 | * 46 | * @param array $attributes 47 | */ 48 | public function addMiddleware(array $attributes) 49 | { 50 | $this->middleware = array_unique(array_merge($this->middleware, array_flatten($attributes))); 51 | } 52 | 53 | /** 54 | * Get field attributes. 55 | * 56 | * @return array 57 | */ 58 | public function getAttributes() 59 | { 60 | return [ 61 | 'namespace' => $this->namespace, 62 | 'middleware' => $this->middleware 63 | ]; 64 | } 65 | 66 | /** 67 | * Attach middleware(s) to field. 68 | * 69 | * @param array|string $middlewares 70 | * @return self 71 | */ 72 | public function middleware($middlewares) 73 | { 74 | $middlewares = is_array($middlewares) ? $middlewares : [$middlewares]; 75 | 76 | foreach ($middlewares as $middlware) { 77 | $this->attachMiddleware($middlware); 78 | } 79 | 80 | return $this; 81 | } 82 | 83 | /** 84 | * Add middleware to collection. 85 | * 86 | * @param string $middleware 87 | * @return void 88 | */ 89 | protected function attachMiddleware($middleware) 90 | { 91 | $this->middleware[] = $middleware; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Schema/FieldCollection.php: -------------------------------------------------------------------------------- 1 | map(function ($field, $key) { 17 | return [$key => $field['namespace']]; 18 | }) 19 | ->collapse() 20 | ->all(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Schema/GraphQL.php: -------------------------------------------------------------------------------- 1 | app = $app; 87 | 88 | $this->types = collect(); 89 | $this->queries = collect(); 90 | $this->mutations = collect(); 91 | $this->typeInstances = collect(); 92 | $this->connectionInstances = collect(); 93 | $this->edgeInstances = collect(); 94 | } 95 | 96 | /** 97 | * Execute GraphQL query. 98 | * 99 | * @param string $query 100 | * @param array $variables 101 | * @param mixed $rootValue 102 | * @return array 103 | */ 104 | public function query($query, $variables = [], $rootValue = null) 105 | { 106 | $result = $this->queryAndReturnResult($query, $variables, $rootValue); 107 | 108 | if (!empty($result->errors)) { 109 | return [ 110 | 'data' => $result->data, 111 | 'errors' => array_map([$this, 'formatError'], $result->errors) 112 | ]; 113 | } 114 | 115 | return ['data' => $result->data]; 116 | } 117 | 118 | /** 119 | * Execute GraphQL query. 120 | * 121 | * @param string $query 122 | * @param array $variables 123 | * @param mixed $rootValue 124 | * @return array 125 | */ 126 | public function queryAndReturnResult($query, $variables = [], $rootValue = null) 127 | { 128 | return GraphQLBase::executeAndReturnResult($this->schema(), $query, $rootValue, $variables); 129 | } 130 | 131 | /** 132 | * Generate GraphQL Schema. 133 | * 134 | * @return \GraphQL\Schema 135 | */ 136 | public function schema() 137 | { 138 | $schema = config('relay.schema'); 139 | 140 | $this->types->each(function ($type, $key) { 141 | $this->type($key); 142 | }); 143 | 144 | $queries = $this->queries->merge(array_get($schema, 'queries', [])); 145 | $mutations = $this->mutations->merge(array_get($schema, 'mutations', [])); 146 | 147 | $queryTypes = $this->generateType($queries, ['name' => 'Query']); 148 | $mutationTypes = $this->generateType($mutations, ['name' => 'Mutation']); 149 | 150 | return new Schema($queryTypes, $mutationTypes); 151 | } 152 | 153 | /** 154 | * Generate type from collection of fields. 155 | * 156 | * @param Collection $fields 157 | * @param array $options 158 | * @return \GraphQL\Type\Definition\ObjectType 159 | */ 160 | public function generateType(Collection $fields, $options = []) 161 | { 162 | $typeFields = $fields->transform(function ($field) { 163 | if (is_string($field)) { 164 | return app($field)->toArray(); 165 | } 166 | 167 | return $field; 168 | })->toArray(); 169 | 170 | return new ObjectType(array_merge(['fields' => $typeFields], $options)); 171 | } 172 | 173 | /** 174 | * Add mutation to collection. 175 | * 176 | * @param string $name 177 | * @param mixed $mutator 178 | */ 179 | public function addMutation($name, $mutator) 180 | { 181 | $this->mutations->put($name, $mutator); 182 | } 183 | 184 | /** 185 | * Add query to collection. 186 | * 187 | * @param string $name 188 | * @param mixed $query 189 | */ 190 | public function addQuery($name, $query) 191 | { 192 | $this->queries->put($name, $query); 193 | } 194 | 195 | /** 196 | * Add type to collection. 197 | * 198 | * @param mixed $class 199 | * @param string|null $name 200 | */ 201 | public function addType($class, $name = null) 202 | { 203 | if (!$name) { 204 | $type = is_object($class) ? $class : app($class); 205 | $name = $type->name; 206 | } 207 | 208 | $this->types->put($name, $class); 209 | } 210 | 211 | /** 212 | * Get instance of type. 213 | * 214 | * @param string $name 215 | * @param boolean $fresh 216 | * @return mixed 217 | */ 218 | public function type($name, $fresh = false) 219 | { 220 | $this->checkType($name); 221 | 222 | if (!$fresh && $this->typeInstances->has($name)) { 223 | return $this->typeInstances->get($name); 224 | } 225 | 226 | $type = $this->types->get($name); 227 | if (!is_object($type)) { 228 | $type = app($type); 229 | } 230 | 231 | $instance = $type instanceof Model ? (new EloquentType($type, $name))->toType() : $type->toType(); 232 | 233 | $this->typeInstances->put($name, $instance); 234 | 235 | if ($type->interfaces) { 236 | InterfaceType::addImplementationToInterfaces($instance); 237 | } 238 | 239 | return $instance; 240 | } 241 | 242 | /** 243 | * Get if type is registered. 244 | * 245 | * @param string $name 246 | * @return boolean 247 | */ 248 | public function hasType($name) 249 | { 250 | return $this->typeInstances->has($name); 251 | } 252 | 253 | /** 254 | * Get registered type. 255 | * 256 | * @param string $name 257 | * @return \GraphQL\Type\Definition\OutputType 258 | */ 259 | public function getType($name) 260 | { 261 | return $this->typeInstances->get($name); 262 | } 263 | 264 | /** 265 | * Get instance of connection type. 266 | * 267 | * @param string $name 268 | * @param Closure|string|null $resolve 269 | * @param boolean $fresh 270 | * @return mixed 271 | */ 272 | public function connection($name, $resolve = null, $fresh = false) 273 | { 274 | $this->checkType($name); 275 | 276 | if ($resolve && !$resolve instanceof Closure) { 277 | $resolve = function ($root, array $args, ResolveInfo $info) use ($resolve) { 278 | return $this->resolveConnection($root, $args, $info, $resolve); 279 | }; 280 | } 281 | 282 | if (!$fresh && $this->connectionInstances->has($name)) { 283 | $field = $this->connectionInstances->get($name); 284 | $field['resolve'] = $resolve; 285 | 286 | return $field; 287 | } 288 | 289 | $field = $this->connectionField($name, $resolve); 290 | 291 | $this->connectionInstances->put($name, $field); 292 | 293 | return $field; 294 | } 295 | 296 | /** 297 | * Get instance of edge type. 298 | * 299 | * @param string $name 300 | * @return \GraphQL\Type\Definition\ObjectType|null 301 | */ 302 | public function edge($name) 303 | { 304 | $this->checkType($name); 305 | 306 | return $this->edgeInstances->get($name); 307 | } 308 | 309 | /** 310 | * Format error for output. 311 | * 312 | * @param Error $e 313 | * @return array 314 | */ 315 | public function formatError(Error $e) 316 | { 317 | $error = ['message' => $e->getMessage()]; 318 | 319 | $locations = $e->getLocations(); 320 | if (!empty($locations)) { 321 | $error['locations'] = array_map(function ($location) { 322 | return $location->toArray(); 323 | }, $locations); 324 | } 325 | 326 | $previous = $e->getPrevious(); 327 | if ($previous && $previous instanceof ValidationError) { 328 | $error['validation'] = $previous->getValidatorMessages(); 329 | } 330 | 331 | return $error; 332 | } 333 | 334 | /** 335 | * Check if type is registered. 336 | * 337 | * @param string $name 338 | * @return void 339 | */ 340 | protected function checkType($name) 341 | { 342 | if (!$this->types->has($name)) { 343 | throw new \Exception("Type [{$name}] not found."); 344 | } 345 | } 346 | 347 | /** 348 | * Generate connection field. 349 | * 350 | * @param string $name 351 | * @param Closure|null $resolve 352 | * @return array 353 | */ 354 | public function connectionField($name, $resolve = null) 355 | { 356 | $type = new RelayConnectionType(); 357 | $connectionName = (!preg_match('/Connection$/', $name)) ? $name.'Connection' : $name; 358 | 359 | $type->setName(studly_case($connectionName)); 360 | $type->setEdgeType($name); 361 | $instance = $type->toType(); 362 | $this->addEdge($instance, $name); 363 | 364 | $field = [ 365 | 'args' => RelayConnectionType::connectionArgs(), 366 | 'type' => $instance, 367 | 'resolve' => $resolve 368 | ]; 369 | 370 | if ($type->interfaces) { 371 | InterfaceType::addImplementationToInterfaces($instance); 372 | } 373 | 374 | return $field; 375 | } 376 | 377 | /** 378 | * Add edge instance. 379 | * 380 | * @param ObjectType $type 381 | * @param string $name 382 | * @return void 383 | */ 384 | public function addEdge(ObjectType $type, $name) 385 | { 386 | if ($edges = $type->getField('edges')) { 387 | $type = $edges->getType()->getWrappedType(); 388 | 389 | $this->edgeInstances->put($name, $type); 390 | } 391 | } 392 | 393 | /** 394 | * Auto-resolve connection. 395 | * 396 | * @param mixed $root 397 | * @param array $args 398 | * @param ResolveInfo $info 399 | * @param string $name 400 | * @return mixed 401 | */ 402 | public function resolveConnection($root, array $args, ResolveInfo $info, $name = '') 403 | { 404 | return $this->getConnectionResolver()->resolve($root, $args, $info, $name); 405 | } 406 | 407 | /** 408 | * Set instance of connection resolver. 409 | * 410 | * @param ConnectionResolver $resolver 411 | */ 412 | public function setConnectionResolver(ConnectionResolver $resolver) 413 | { 414 | $this->connectionResolver = $resolver; 415 | } 416 | 417 | /** 418 | * Get instance of connection resolver. 419 | * 420 | * @return ConnectionResolver 421 | */ 422 | public function getConnectionResolver() 423 | { 424 | return $this->connectionResolver ?: app(ConnectionResolver::class); 425 | } 426 | 427 | /** 428 | * Get instance of cache store. 429 | * 430 | * @return \Nuwave\Relay\Support\Cache\FileStore 431 | */ 432 | public function cache() 433 | { 434 | return $this->cache ?: app(\Nuwave\Relay\Support\Cache\FileStore::class); 435 | } 436 | 437 | /** 438 | * Set instance of Cache store. 439 | * 440 | * @param \Nuwave\Relay\Support\Cache\FileStore 441 | */ 442 | public function setCache(FileStore $cache) 443 | { 444 | $this->cache = $cache; 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /src/Schema/Parser.php: -------------------------------------------------------------------------------- 1 | initialize(); 59 | 60 | $this->parseFields($selectionSet, $root); 61 | 62 | return $this->connections; 63 | } 64 | 65 | /** 66 | * Set the selection set. 67 | * 68 | * @return void 69 | */ 70 | public function initialize() 71 | { 72 | $this->depth = 0; 73 | $this->path = []; 74 | $this->connections = []; 75 | } 76 | 77 | /** 78 | * Determine if field has selection set. 79 | * 80 | * @param Field $field 81 | * @return boolean 82 | */ 83 | protected function hasChildren($field) 84 | { 85 | return $this->isField($field) && isset($field->selectionSet) && !empty($field->selectionSet->selections); 86 | } 87 | 88 | /** 89 | * Determine if name is a relay edge. 90 | * 91 | * @param string $name 92 | * @return boolean 93 | */ 94 | protected function isEdge($name) 95 | { 96 | return in_array($name, $this->relayEdges); 97 | } 98 | 99 | /** 100 | * Parse arguments. 101 | * 102 | * @param array $selectionSet 103 | * @param string $root 104 | * @return void 105 | */ 106 | protected function parseFields(array $selectionSet = [], $root = '') 107 | { 108 | foreach ($selectionSet as $field) { 109 | if ($this->hasChildren($field)) { 110 | $name = $field->name->value;; 111 | 112 | if (!$this->isEdge($name)) { 113 | $this->path[] = $name; 114 | 115 | $connection = new Connection; 116 | $connection->name = $name; 117 | $connection->root = $root; 118 | $connection->path = implode('.', $this->path); 119 | $connection->depth = count($this->path); 120 | $connection->setArguments($field); 121 | 122 | $this->connections[] = $connection; 123 | } 124 | 125 | $this->parseFields($field->selectionSet->selections, $root); 126 | } 127 | } 128 | 129 | array_pop($this->path); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Schema/SchemaContainer.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 76 | 77 | $this->mutations = new Collection; 78 | $this->queries = new Collection; 79 | $this->types = new Collection; 80 | } 81 | 82 | /** 83 | * Set up the graphql request. 84 | * 85 | * @param $query string 86 | * @return void 87 | */ 88 | public function setupRequest($query = 'GraphGL request', $operation = 'query') 89 | { 90 | $source = new Source($query); 91 | $ast = GraphQLParser::parse($source); 92 | 93 | if (isset($ast->definitions[0])) { 94 | $d = $ast->definitions[0]; 95 | $operation = $d->operation ?: 'query'; 96 | $selectionSet = $d->selectionSet->selections; 97 | 98 | $this->parseSelections($selectionSet, $operation); 99 | } 100 | } 101 | 102 | /** 103 | * Check to see if field is a parent. 104 | * 105 | * @param string $name 106 | * @return boolean 107 | */ 108 | public function isParent($name) 109 | { 110 | foreach ($this->connections as $connection) { 111 | if ($this->hasPath($connection, $name)) { 112 | return true; 113 | } 114 | } 115 | 116 | return false; 117 | } 118 | 119 | /** 120 | * Get list of connections in query that belong 121 | * to parent. 122 | * 123 | * @param string $parent 124 | * @param array $connections 125 | * @return array 126 | */ 127 | public function connectionsInRequest($parent, array $connections) 128 | { 129 | $queryConnections = []; 130 | 131 | foreach ($this->connections as $connection) { 132 | if ($this->hasPath($connection, $parent) && isset($connections[$connection->name])) { 133 | $queryConnections[] = $connections[$connection->name]; 134 | } 135 | } 136 | 137 | return $queryConnections; 138 | } 139 | 140 | /** 141 | * Get arguments of connection. 142 | * 143 | * @param string $name 144 | * @return array 145 | */ 146 | public function connectionArguments($name) 147 | { 148 | $connection = array_first($this->connections, function ($key, $connection) use ($name) { 149 | return $connection->name == $name; 150 | }); 151 | 152 | if ($connection) { 153 | return $connection->arguments; 154 | } 155 | 156 | return []; 157 | } 158 | 159 | /** 160 | * Determine if connection has parent in it's path. 161 | * 162 | * @param Connection $connection 163 | * @param string $parent 164 | * @return boolean 165 | */ 166 | protected function hasPath(Connection $connection, $parent) 167 | { 168 | return preg_match("/{$parent}./", $connection->path); 169 | } 170 | 171 | /** 172 | * Add connection to collection. 173 | * 174 | * @param string $name 175 | * @param string $namespace 176 | * @return Field 177 | */ 178 | public function connection($name, $namespace) 179 | { 180 | $edgeType = $this->createField($name.'Edge', $namespace); 181 | 182 | $this->types->push($edgeType); 183 | 184 | $connectionType = $this->createField($name.'Connection', $namespace); 185 | 186 | $this->types->push($connectionType); 187 | 188 | return [ 189 | 'connectionType' => $connectionType, 190 | 'edgeType' => $edgeType, 191 | ]; 192 | } 193 | 194 | /** 195 | * Add mutation to collection. 196 | * 197 | * @param string $name 198 | * @param array $namespace 199 | * @return Field 200 | */ 201 | public function mutation($name, $namespace) 202 | { 203 | $mutation = $this->createField($name, $namespace); 204 | 205 | $this->mutations->push($mutation); 206 | 207 | return $mutation; 208 | } 209 | 210 | /** 211 | * Add query to collection. 212 | * 213 | * @param string $name 214 | * @param array $namespace 215 | * @return Field 216 | */ 217 | public function query($name, $namespace) 218 | { 219 | $query = $this->createField($name, $namespace); 220 | 221 | $this->queries->push($query); 222 | 223 | return $query; 224 | } 225 | 226 | /** 227 | * Add type to collection. 228 | * 229 | * @param string $name 230 | * @param string $namespace 231 | * @return Field 232 | */ 233 | public function type($name, $namespace) 234 | { 235 | $type = $this->createField($name, $namespace); 236 | 237 | $this->types->push($type); 238 | 239 | return $type; 240 | } 241 | 242 | /** 243 | * Group child elements. 244 | * 245 | * @param array $attributes 246 | * @param Closure $callback 247 | * @return void 248 | */ 249 | public function group(array $attributes, Closure $callback) 250 | { 251 | $oldNamespace = $this->namespace; 252 | 253 | if (isset($attributes['middleware'])) { 254 | $this->middlewareStack[] = $attributes['middleware']; 255 | } 256 | 257 | if (isset($attributes['namespace'])) { 258 | $this->namespace .= '\\' . trim($attributes['namespace'], '\\'); 259 | } 260 | 261 | $callback(); 262 | 263 | if (isset($attributes['middleware'])) { 264 | array_pop($this->middlewareStack); 265 | } 266 | 267 | if (isset($attributes['namespace'])) { 268 | $this->namespace = $oldNamespace; 269 | } 270 | } 271 | 272 | /** 273 | * Get mutations. 274 | * 275 | * @return Collection 276 | */ 277 | public function getMutations() 278 | { 279 | return $this->mapFields($this->mutations); 280 | } 281 | 282 | /** 283 | * Get queries. 284 | * 285 | * @return Collection 286 | */ 287 | public function getQueries() 288 | { 289 | return $this->mapFields($this->queries); 290 | } 291 | 292 | /** 293 | * Get queries. 294 | * 295 | * @return Collection 296 | */ 297 | public function getTypes() 298 | { 299 | return $this->mapFields($this->types); 300 | } 301 | 302 | /** 303 | * Transform fields into collapsed collection. 304 | * 305 | * @param Collection $collection 306 | * @return Collection 307 | */ 308 | public function mapFields(Collection $collection) 309 | { 310 | return $collection->map(function ($field) { 311 | return [ 312 | $field->name => $field->getAttributes() 313 | ]; 314 | })->collapse(); 315 | } 316 | 317 | /** 318 | * Find by operation type and name. 319 | * 320 | * @param string $name 321 | * @param string $operation 322 | * @return array 323 | */ 324 | public function find($name, $operation = 'query') 325 | { 326 | if ($operation == 'mutation') { 327 | return $this->findMutation($name); 328 | } elseif ($operation == 'type') { 329 | return $this->findType($name); 330 | } 331 | 332 | return $this->findQuery($name); 333 | } 334 | 335 | /** 336 | * Find mutation by name. 337 | * 338 | * @param string $name 339 | * @return array 340 | */ 341 | public function findMutation($name) 342 | { 343 | return $this->getMutations()->pull($name); 344 | } 345 | 346 | /** 347 | * Find query by name. 348 | * 349 | * @param string $name 350 | * @return array 351 | */ 352 | public function findQuery($name) 353 | { 354 | return $this->getQueries()->pull($name); 355 | } 356 | 357 | /** 358 | * Find type by name. 359 | * 360 | * @param string $name 361 | * @return array 362 | */ 363 | public function findType($name) 364 | { 365 | return $this->getTypes()->pull($name); 366 | } 367 | 368 | /** 369 | * Get the middlware for the query. 370 | * 371 | * @return array 372 | */ 373 | public function middleware() 374 | { 375 | return $this->middleware; 376 | } 377 | 378 | /** 379 | * Get connections for the query. 380 | * 381 | * @return \Illuminate\Support\Collection 382 | */ 383 | public function connections() 384 | { 385 | return collect($this->connections); 386 | } 387 | 388 | /** 389 | * Get connection paths to eager load. 390 | * 391 | * @return array 392 | */ 393 | public function eagerLoad() 394 | { 395 | return $this->connections()->pluck('path')->toArray(); 396 | } 397 | 398 | /** 399 | * Initialize schema. 400 | * 401 | * @param array $selectionSet 402 | * @return void 403 | */ 404 | protected function parseSelections(array $selectionSet = [], $operation = '') 405 | { 406 | foreach ($selectionSet as $selection) { 407 | if ($this->parser->isField($selection)) { 408 | $schema = $this->find($selection->name->value, $operation); 409 | 410 | if (isset($schema['middleware']) && !empty($schema['middleware'])) { 411 | $this->middleware = array_merge($this->middleware, $schema['middleware']); 412 | } 413 | 414 | if (isset($selection->selectionSet) && !empty($selection->selectionSet->selections)) { 415 | $this->connections = array_merge( 416 | $this->connections, 417 | $this->parser->getConnections( 418 | $selection->selectionSet->selections, 419 | $selection->name->value 420 | ) 421 | ); 422 | } 423 | } 424 | } 425 | } 426 | 427 | /** 428 | * Get class name. 429 | * 430 | * @param string $namespace 431 | * @return string 432 | */ 433 | protected function getClassName($namespace) 434 | { 435 | return empty(trim($this->namespace)) ? $namespace : trim($this->namespace, '\\') . '\\' . $namespace; 436 | } 437 | 438 | /** 439 | * Get field and attach necessary middleware. 440 | * 441 | * @param string $name 442 | * @param string $namespace 443 | * @return Field 444 | */ 445 | protected function createField($name, $namespace) 446 | { 447 | $field = new Field($name, $this->getClassName($namespace)); 448 | 449 | if ($this->hasMiddlewareStack()) { 450 | $field->addMiddleware($this->middlewareStack); 451 | } 452 | 453 | return $field; 454 | } 455 | 456 | /** 457 | * Check if middleware stack is empty. 458 | * 459 | * @return boolean 460 | */ 461 | protected function hasMiddlewareStack() 462 | { 463 | return !empty($this->middlewareStack); 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /src/Support/Cache/FileStore.php: -------------------------------------------------------------------------------- 1 | getPath($name); 17 | 18 | $this->makeDir(dirname($path)); 19 | 20 | return file_put_contents($path, serialize($data)); 21 | } 22 | 23 | /** 24 | * Retrieve data from cache. 25 | * 26 | * @param string $name 27 | * @return mixed|null 28 | */ 29 | public function get($name) 30 | { 31 | if (file_exists($this->getPath($name))) { 32 | return unserialize(file_get_contents($this->getPath($name))); 33 | } 34 | 35 | return null; 36 | } 37 | 38 | /** 39 | * Remove the cache directory. 40 | * 41 | * @return void 42 | */ 43 | public function flush() 44 | { 45 | $path = $this->getPath(''); 46 | 47 | if (file_exists($path)) { 48 | collect(array_diff(scandir($path), ['..', '.']))->each(function ($file) { 49 | unlink($this->getPath($file)); 50 | }); 51 | } 52 | } 53 | 54 | /** 55 | * Get path name of item. 56 | * 57 | * @param string $name 58 | * @return string 59 | */ 60 | protected function getPath($name) 61 | { 62 | return storage_path('graphql/cache/'.strtolower($name)); 63 | } 64 | 65 | /** 66 | * Make a directory tree recursively. 67 | * 68 | * @param string $dir 69 | * @return void 70 | */ 71 | public function makeDir($dir) 72 | { 73 | if (! is_dir($dir)) { 74 | mkdir($dir, 0777, true); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Support/ConnectionResolver.php: -------------------------------------------------------------------------------- 1 | getItems($root, $info, $name); 26 | 27 | if (isset($args['first'])) { 28 | $total = $items->count(); 29 | $first = $args['first']; 30 | $after = $this->decodeCursor($args); 31 | $currentPage = $first && $after ? floor(($first + $after) / $first) : 1; 32 | 33 | return new Paginator( 34 | $items->slice($after)->take($first), 35 | $total, 36 | $first, 37 | $currentPage 38 | ); 39 | } 40 | 41 | return new Paginator( 42 | $items, 43 | count($items), 44 | (count($items) > 0 ? count($items) : 1) 45 | ); 46 | } 47 | 48 | /** 49 | * @param $collection 50 | * @param ResolveInfo $info 51 | * @param $name 52 | * @return mixed|Collection 53 | */ 54 | protected function getItems($collection, ResolveInfo $info, $name) 55 | { 56 | $items = []; 57 | 58 | if ($collection instanceof Model) { 59 | if (in_array($name, array_keys($collection->getRelations()))) { 60 | return $collection->{$name}; 61 | } 62 | 63 | $items = method_exists($collection, $name) 64 | ? $collection->{$name}()->get() //->select(...$this->getSelectFields($info))->get() 65 | : $collection->getAttribute($name); 66 | return $items; 67 | } elseif (is_object($collection) && method_exists($collection, 'get')) { 68 | $items = $collection->get($name); 69 | return $items; 70 | } elseif (is_array($collection) && isset($collection[$name])) { 71 | return collect($collection[$name]); 72 | } 73 | 74 | return $items; 75 | } 76 | 77 | /** 78 | * Select only certain fields on queries instead of all fields. 79 | * 80 | * @param ResolveInfo $info 81 | * @return array 82 | */ 83 | protected function getSelectFields(ResolveInfo $info) 84 | { 85 | $camel = config('relay.camel_case'); 86 | 87 | return collect($info->getFieldSelection(4)['edges']['node']) 88 | ->reject(function ($value) { 89 | is_array($value); 90 | })->keys()->transform(function ($value) use ($camel) { 91 | if ($camel) { 92 | return snake_case($value); 93 | } 94 | 95 | return $value; 96 | })->toArray(); 97 | } 98 | 99 | /** 100 | * Decode cursor from query arguments. 101 | * 102 | * @param array $args 103 | * @return integer 104 | */ 105 | public function decodeCursor(array $args) 106 | { 107 | return isset($args['after']) ? $this->getCursorId($args['after']) : 0; 108 | } 109 | 110 | /** 111 | * Get id from encoded cursor. 112 | * 113 | * @param string $cursor 114 | * @return integer 115 | */ 116 | protected function getCursorId($cursor) 117 | { 118 | return (int)$this->decodeRelayId($cursor); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Support/Definition/EdgeType.php: -------------------------------------------------------------------------------- 1 | name = $name; 37 | $this->type = $type; 38 | } 39 | 40 | /** 41 | * Fields that exist on every connection. 42 | * 43 | * @return array 44 | */ 45 | public function fields() 46 | { 47 | return [ 48 | 'node' => [ 49 | 'type' => function () { 50 | if (is_object($this->type)) { 51 | return $this->type; 52 | } 53 | 54 | return $this->getNodeType($this->type); 55 | }, 56 | 'description' => 'The item at the end of the edge.', 57 | 'resolve' => function ($edge) { 58 | return $edge; 59 | }, 60 | ], 61 | 'cursor' => [ 62 | 'type' => Type::nonNull(Type::string()), 63 | 'description' => 'A cursor for use in pagination.', 64 | 'resolve' => function ($edge) { 65 | if (is_array($edge) && isset($edge['relayCursor'])) { 66 | return $edge['relayCursor']; 67 | } elseif (is_array($edge->attributes)) { 68 | return $edge->attributes['relayCursor']; 69 | } 70 | 71 | return $edge->relayCursor; 72 | }, 73 | ] 74 | ]; 75 | } 76 | 77 | /** 78 | * Convert the Fluent instance to an array. 79 | * 80 | * @return array 81 | */ 82 | public function toArray() 83 | { 84 | return [ 85 | 'name' => ucfirst($this->name).'Edge', 86 | 'description' => 'An edge in a connection.', 87 | 'fields' => $this->fields(), 88 | ]; 89 | } 90 | 91 | /** 92 | * Create the instance of the connection type. 93 | * 94 | * @return ObjectType 95 | */ 96 | public function toType() 97 | { 98 | return new ObjectType($this->toArray()); 99 | } 100 | 101 | /** 102 | * Get node at the end of the edge. 103 | * 104 | * @param string $name 105 | * @return \GraphQL\Type\Definition\OutputType 106 | */ 107 | protected function getNodeType($name) 108 | { 109 | $graphql = app('graphql'); 110 | 111 | return $graphql->hasType($this->type) ? $graphql->getType($this->type) : $graphql->type($this->type); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Support/Definition/EloquentType.php: -------------------------------------------------------------------------------- 1 | name = $name; 58 | $this->fields = collect(); 59 | $this->hiddenFields = collect($model->getHidden())->flip(); 60 | $this->model = $model; 61 | $this->camelCase = config('relay.camel_case', false); 62 | } 63 | 64 | /** 65 | * Transform eloquent model to graphql type. 66 | * 67 | * @return \GraphQL\Type\Definition\ObjectType 68 | */ 69 | public function toType() 70 | { 71 | $graphql = app('graphql'); 72 | $name = $this->getName(); 73 | 74 | if ($fields = $graphql->cache()->get($name)) { 75 | $this->fields = $fields; 76 | } else { 77 | $this->schemaFields(); 78 | $graphql->cache()->store($name, $this->fields); 79 | } 80 | 81 | if (method_exists($this->model, 'graphqlFields')) { 82 | $this->eloquentFields(collect($this->model->graphqlFields())); 83 | } 84 | 85 | if (method_exists($this->model, $this->getTypeMethod())) { 86 | $method = $this->getTypeMethod(); 87 | $this->eloquentFields(collect($this->model->{$method}())); 88 | } 89 | 90 | return new ObjectType([ 91 | 'name' => $name, 92 | 'description' => $this->getDescription(), 93 | 'fields' => $this->fields->toArray() 94 | ]); 95 | } 96 | 97 | /** 98 | * Get fields for model. 99 | * 100 | * @return \Illuminate\Support\DefinitionsCollection 101 | */ 102 | public function rawFields() 103 | { 104 | $this->schemaFields(); 105 | 106 | if (method_exists($this->model, 'graphqlFields')) { 107 | $this->eloquentFields(collect($this->model->graphqlFields())); 108 | } 109 | 110 | if (method_exists($this->model, $this->getTypeMethod())) { 111 | $method = $this->getTypeMethod(); 112 | $this->eloquentFields(collect($this->model->{$method}())); 113 | } 114 | 115 | return $this->fields->transform(function ($field, $key) { 116 | $field['type'] = $this->getRawType($field['type']); 117 | 118 | return $field; 119 | }); 120 | } 121 | 122 | /** 123 | * Convert eloquent defined fields. 124 | * 125 | * @param \Illuminate\Support\Collection 126 | * @return array 127 | */ 128 | public function eloquentFields(Collection $fields) 129 | { 130 | $fields->each(function ($field, $key) { 131 | if (!$this->skipField($key)) { 132 | $data = []; 133 | $data['type'] = $field['type']; 134 | $data['description'] = isset($field['description']) ? $field['description'] : null; 135 | 136 | if (isset($field['resolve'])) { 137 | $data['resolve'] = $field['resolve']; 138 | } elseif ($method = $this->getModelResolve($key)) { 139 | $data['resolve'] = $method; 140 | } 141 | 142 | $this->addField($key, $field); 143 | } 144 | }); 145 | } 146 | 147 | /** 148 | * Create fields for type. 149 | * 150 | * @return void 151 | */ 152 | protected function schemaFields() 153 | { 154 | $table = $this->model->getTable(); 155 | $schema = $this->model->getConnection()->getSchemaBuilder(); 156 | $columns = collect($schema->getColumnListing($table)); 157 | 158 | $columns->each(function ($column) use ($table, $schema) { 159 | if (!$this->skipField($column)) { 160 | $this->generateField( 161 | $column, 162 | $schema->getColumnType($table, $column) 163 | ); 164 | } 165 | }); 166 | } 167 | 168 | /** 169 | * Generate type field from schema. 170 | * 171 | * @param string $name 172 | * @param string $colType 173 | * @return void 174 | */ 175 | protected function generateField($name, $colType) 176 | { 177 | $field = []; 178 | $field['type'] = $this->resolveTypeByColumn($name, $colType); 179 | $field['description'] = isset($this->descriptions['name']) ? $this->descriptions[$name] : null; 180 | 181 | if ($name === $this->model->getKeyName()) { 182 | $field['description'] = $field['description'] ?: 'Primary id of type.'; 183 | } 184 | 185 | if ($method = $this->getModelResolve($name)) { 186 | $field['resolve'] = $method; 187 | } 188 | 189 | $fieldName = $this->camelCase ? camel_case($name) : $name; 190 | 191 | $this->addField($fieldName, $field); 192 | } 193 | 194 | /** 195 | * Resolve field type by column info. 196 | * 197 | * @param string $name 198 | * @param string $colType 199 | * @return \GraphQL\Type\Definition\Type 200 | */ 201 | protected function resolveTypeByColumn($name, $colType) 202 | { 203 | $type = Type::string(); 204 | $type->name = $this->getName().'_String'; 205 | 206 | if ($name === $this->model->getKeyName()) { 207 | $type = Type::id(); 208 | $type->name = $this->getName().'_ID'; 209 | } elseif ($colType === 'integer') { 210 | $type = Type::int(); 211 | $type->name = $this->getName().'_Int'; 212 | } elseif ($colType === 'float' || $colType === 'decimal') { 213 | $type = Type::float(); 214 | $type->name = $this->getName().'_Float'; 215 | } elseif ($colType === 'boolean') { 216 | $type = Type::boolean(); 217 | $type->name = $this->getName().'_Boolean'; 218 | } 219 | 220 | return $type; 221 | } 222 | 223 | /** 224 | * Get raw name for type. 225 | * 226 | * @param Type $type 227 | * @return string 228 | */ 229 | protected function getRawType(Type $type) 230 | { 231 | $class = get_class($type); 232 | $namespace = 'GraphQL\\Type\\Definition\\'; 233 | 234 | if ($class == $namespace . 'NonNull') { 235 | return 'Type::nonNull('. $this->getRawType($type->getWrappedType()) .')'; 236 | } elseif ($class == $namespace . 'IDType') { 237 | return 'Type::id()'; 238 | } elseif ($class == $namespace . 'IntType') { 239 | return 'Type::int()'; 240 | } elseif ($class == $namespace . 'BooleanType') { 241 | return 'Type::bool()'; 242 | } elseif ($class == $namespace . 'FloatType') { 243 | return 'Type::float()'; 244 | } 245 | 246 | return 'Type::string()'; 247 | } 248 | 249 | /** 250 | * Add field to collection. 251 | * 252 | * @param string $name 253 | * @param array $field 254 | */ 255 | protected function addField($name, $field) 256 | { 257 | $name = $this->camelCase ? camel_case($name) : $name; 258 | 259 | $this->fields->put($name, $field); 260 | } 261 | 262 | /** 263 | * Check if field should be skipped. 264 | * 265 | * @param string $field 266 | * @return boolean 267 | */ 268 | protected function skipField($name = '') 269 | { 270 | if ($this->hiddenFields->has($name) || $this->fields->has($name)) { 271 | return true; 272 | } 273 | 274 | return false; 275 | } 276 | 277 | /** 278 | * Check if model has resolve function. 279 | * 280 | * @param string $key 281 | * @return string|null 282 | */ 283 | protected function getModelResolve($key) 284 | { 285 | $method = 'resolve' . studly_case($key) . 'Field'; 286 | 287 | if (method_exists($this->model, $method)) { 288 | return array($this->model, $method); 289 | } 290 | 291 | return null; 292 | } 293 | 294 | /** 295 | * Get name for type. 296 | * 297 | * @return string 298 | */ 299 | protected function getName() 300 | { 301 | if ($this->name) { 302 | return studly_case($this->name); 303 | } 304 | 305 | return $this->model->name ?: ucfirst((new ReflectionClass($this->model))->getShortName()); 306 | } 307 | 308 | /** 309 | * Get description of type. 310 | * 311 | * @return string 312 | */ 313 | protected function getDescription() 314 | { 315 | return $this->model->description ?: null; 316 | } 317 | 318 | /** 319 | * Get method name for type. 320 | * 321 | * @return string 322 | */ 323 | protected function getTypeMethod() 324 | { 325 | return 'graphql'.$this->getName().'Fields'; 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/Support/Definition/GraphQLField.php: -------------------------------------------------------------------------------- 1 | attributes, [ 63 | 'args' => $this->args() 64 | ], $this->attributes()); 65 | 66 | $attributes['type'] = $this->type(); 67 | 68 | $attributes['resolve'] = $this->getResolver(); 69 | 70 | return $attributes; 71 | } 72 | 73 | /** 74 | * Get rules for field. 75 | * 76 | * @return array 77 | */ 78 | public function getRules() 79 | { 80 | $arguments = func_get_args(); 81 | $args = $this->args(); 82 | 83 | if ($this instanceof RelayMutation) { 84 | $args = $this->inputFields(); 85 | } 86 | 87 | return collect($args) 88 | ->transform(function ($arg, $name) use ($arguments) { 89 | if (isset($arg['rules'])) { 90 | if (is_callable($arg['rules'])) { 91 | return call_user_func_array($arg['rules'], $arguments); 92 | } 93 | return $arg['rules']; 94 | } 95 | return null; 96 | }) 97 | ->merge(call_user_func_array([$this, 'rules'], $arguments)) 98 | ->reject(function ($arg) { 99 | return is_null($arg); 100 | }) 101 | ->toArray(); 102 | } 103 | 104 | /** 105 | * Get the field resolver. 106 | * 107 | * @return \Closure|null 108 | */ 109 | protected function getResolver() 110 | { 111 | if (!method_exists($this, 'resolve')) { 112 | return null; 113 | } 114 | 115 | $resolver = array($this, 'resolve'); 116 | 117 | return function () use ($resolver) { 118 | $arguments = func_get_args(); 119 | $rules = call_user_func_array([$this, 'getRules'], $arguments); 120 | 121 | if (sizeof($rules)) { 122 | $input = $this->getInput($arguments); 123 | $validator = app('validator')->make($input, $rules); 124 | 125 | if ($validator->fails()) { 126 | throw with(new ValidationError('validation'))->setValidator($validator); 127 | } 128 | } 129 | 130 | return call_user_func_array($resolver, $arguments); 131 | }; 132 | } 133 | 134 | /** 135 | * Get input for validation. 136 | * 137 | * @param array $arguments 138 | * @return array 139 | */ 140 | protected function getInput(array $arguments) 141 | { 142 | return array_get($arguments, 1, []); 143 | } 144 | 145 | /** 146 | * Convert the Fluent instance to an array. 147 | * 148 | * @return array 149 | */ 150 | public function toArray() 151 | { 152 | return $this->getAttributes(); 153 | } 154 | 155 | /** 156 | * Transform into field array. 157 | * 158 | * @return array 159 | */ 160 | public static function field() 161 | { 162 | return (new static)->toArray(); 163 | } 164 | 165 | /** 166 | * Dynamically retrieve the value of an attribute. 167 | * 168 | * @param string $key 169 | * @return mixed 170 | */ 171 | public function __get($key) 172 | { 173 | $attributes = $this->getAttributes(); 174 | 175 | return isset($attributes[$key]) ? $attributes[$key]:null; 176 | } 177 | 178 | /** 179 | * Dynamically check if an attribute is set. 180 | * 181 | * @param string $key 182 | * @return bool 183 | */ 184 | public function __isset($key) 185 | { 186 | return isset($this->getAttributes()[$key]); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/Support/Definition/GraphQLInterface.php: -------------------------------------------------------------------------------- 1 | getTypeResolver(); 35 | if(isset($resolver)) 36 | { 37 | $attributes['resolveType'] = $resolver; 38 | } 39 | 40 | return $attributes; 41 | } 42 | 43 | public function toType() 44 | { 45 | return new InterfaceType($this->toArray()); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/Support/Definition/GraphQLMutation.php: -------------------------------------------------------------------------------- 1 | attributes, [ 31 | 'fields' => function () { 32 | return $this->fields(); 33 | } 34 | ]); 35 | 36 | if (sizeof($this->interfaces())) { 37 | $attributes['interfaces'] = $this->interfaces(); 38 | } 39 | 40 | return $attributes; 41 | } 42 | 43 | /** 44 | * The resolver for a specific field. 45 | * 46 | * @param $name 47 | * @param $field 48 | * @return \Closure|null 49 | */ 50 | protected function getFieldResolver($name, $field) 51 | { 52 | if(isset($field['resolve'])) { 53 | return $field['resolve']; 54 | } else if(method_exists($this, 'resolve'.studly_case($name).'Field')) { 55 | $resolver = array($this, 'resolve'.studly_case($name).'Field'); 56 | 57 | return function() use ($resolver) { 58 | return call_user_func_array($resolver, func_get_args()); 59 | }; 60 | } 61 | 62 | return null; 63 | } 64 | 65 | /** 66 | * Get the fields of the type. 67 | * 68 | * @return array 69 | */ 70 | public function getFields() 71 | { 72 | $collection = new Collection($this->fields()); 73 | 74 | return $collection->transform(function ($field, $name) { 75 | if(is_string($field)) { 76 | $field = app($field); 77 | 78 | $field->name = $name; 79 | 80 | return $field->toArray(); 81 | } else { 82 | $resolver = $this->getFieldResolver($name, $field); 83 | 84 | if ($resolver) { 85 | $field['resolve'] = $resolver; 86 | } 87 | 88 | return $field; 89 | } 90 | })->toArray(); 91 | } 92 | 93 | /** 94 | * Type interfaces. 95 | * 96 | * @return array 97 | */ 98 | public function interfaces() 99 | { 100 | return []; 101 | } 102 | 103 | /** 104 | * Convert the object to an array. 105 | * 106 | * @return array 107 | */ 108 | public function toArray() 109 | { 110 | return $this->getAttributes(); 111 | } 112 | 113 | /** 114 | * Convert this class to its ObjectType. 115 | * 116 | * @return ObjectType 117 | */ 118 | public function toType() 119 | { 120 | return new ObjectType($this->toArray()); 121 | } 122 | 123 | /** 124 | * Dynamically retrieve the value of an attribute. 125 | * 126 | * @param string $key 127 | * @return mixed 128 | */ 129 | public function __get($key) 130 | { 131 | $attributes = $this->getAttributes(); 132 | 133 | return isset($attributes[$key]) ? $attributes[$key]:null; 134 | } 135 | 136 | /** 137 | * Dynamically check if an attribute is set. 138 | * 139 | * @param string $key 140 | * @return bool 141 | */ 142 | public function __isset($key) 143 | { 144 | return isset($this->getAttributes()[$key]); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Support/Definition/PageInfoType.php: -------------------------------------------------------------------------------- 1 | 'pageInfo', 22 | 'description' => 'Information to aid in pagination.' 23 | ]; 24 | 25 | /** 26 | * Fields available on PageInfo. 27 | * 28 | * @return array 29 | */ 30 | public function fields() 31 | { 32 | return [ 33 | 'hasNextPage' => [ 34 | 'type' => Type::nonNull(Type::boolean()), 35 | 'description' => 'When paginating forwards, are there more items?', 36 | 'resolve' => function ($collection) { 37 | if ($collection instanceof LengthAwarePaginator) { 38 | return $collection->hasMorePages(); 39 | } 40 | 41 | return false; 42 | } 43 | ], 44 | 'hasPreviousPage' => [ 45 | 'type' => Type::nonNull(Type::boolean()), 46 | 'description' => 'When paginating backwards, are there more items?', 47 | 'resolve' => function ($collection) { 48 | if ($collection instanceof LengthAwarePaginator) { 49 | return $collection->currentPage() > 1; 50 | } 51 | 52 | return false; 53 | } 54 | ], 55 | 'startCursor' => [ 56 | 'type' => Type::string(), 57 | 'description' => 'When paginating backwards, the cursor to continue.', 58 | 'resolve' => function ($collection) { 59 | if ($collection instanceof LengthAwarePaginator) { 60 | return $this->encodeGlobalId( 61 | 'arrayconnection', 62 | $collection->firstItem() * $collection->currentPage() 63 | ); 64 | } 65 | 66 | return null; 67 | } 68 | ], 69 | 'endCursor' => [ 70 | 'type' => Type::string(), 71 | 'description' => 'When paginating forwards, the cursor to continue.', 72 | 'resolve' => function ($collection) { 73 | if ($collection instanceof LengthAwarePaginator) { 74 | return $this->encodeGlobalId( 75 | 'arrayconnection', 76 | $collection->lastItem() * $collection->currentPage() 77 | ); 78 | } 79 | 80 | return null; 81 | } 82 | ] 83 | ]; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Support/Definition/RelayConnectionType.php: -------------------------------------------------------------------------------- 1 | type ?: $this->type(); 65 | 66 | return [ 67 | 'pageInfo' => [ 68 | 'type' => Type::nonNull(GraphQL::type('pageInfo')), 69 | 'description' => 'Information to aid in pagination.', 70 | 'resolve' => function ($collection) { 71 | return $collection; 72 | }, 73 | ], 74 | 'edges' => [ 75 | 'type' => Type::listOf($this->buildEdgeType($this->name, $type)), 76 | 'description' => 'Information to aid in pagination.', 77 | 'resolve' => function ($collection) { 78 | return $this->injectCursor($collection); 79 | }, 80 | ] 81 | ]; 82 | } 83 | 84 | /** 85 | * Get the default arguments for a connection. 86 | * 87 | * @return array 88 | */ 89 | public static function connectionArgs() 90 | { 91 | return [ 92 | 'after' => [ 93 | 'type' => Type::string() 94 | ], 95 | 'first' => [ 96 | 'type' => Type::int() 97 | ], 98 | 'before' => [ 99 | 'type' => Type::string() 100 | ], 101 | 'last' => [ 102 | 'type' => Type::int() 103 | ] 104 | ]; 105 | } 106 | 107 | /** 108 | * Build the edge type for this connection. 109 | * 110 | * @param $name 111 | * @param $type 112 | * @return ObjectType 113 | */ 114 | protected function buildEdgeType($name, $type) 115 | { 116 | if (preg_match('/Connection$/', $name)) { 117 | $name = substr($name, 0, strlen($name) - 10); 118 | } 119 | 120 | $edge = new EdgeType($name, $type); 121 | 122 | return $edge->toType(); 123 | } 124 | 125 | /** 126 | * Inject encoded cursor into collection items. 127 | * 128 | * @param mixed $collection 129 | * @return mixed 130 | */ 131 | protected function injectCursor($collection) 132 | { 133 | if ($collection instanceof LengthAwarePaginator) { 134 | $page = $collection->currentPage(); 135 | 136 | $collection->values()->each(function ($item, $x) use ($page) { 137 | $cursor = ($x + 1) * $page; 138 | $encodedCursor = $this->encodeGlobalId('arrayconnection', $cursor); 139 | if (is_array($item)) { 140 | $item['relayCursor'] = $encodedCursor; 141 | } else { 142 | if (is_object($item) && is_array($item->attributes)) { 143 | $item->attributes['relayCursor'] = $encodedCursor; 144 | } else { 145 | $item->relayCursor = $encodedCursor; 146 | } 147 | } 148 | }); 149 | } 150 | 151 | return $collection; 152 | } 153 | 154 | /** 155 | * Get id from encoded cursor. 156 | * 157 | * @param string $cursor 158 | * @return integer 159 | */ 160 | protected function getCursorId($cursor) 161 | { 162 | return (int)$this->decodeRelayId($cursor); 163 | } 164 | 165 | /** 166 | * Convert the Fluent instance to an array. 167 | * 168 | * @return array 169 | */ 170 | public function toArray() 171 | { 172 | $fields = array_merge($this->baseFields(), $this->fields()); 173 | 174 | return [ 175 | 'name' => ucfirst($this->name), 176 | 'description' => 'A connection to a list of items.', 177 | 'fields' => $fields, 178 | 'resolve' => function ($root, $args, ResolveInfo $info) { 179 | return $this->resolve($root, $args, $info, $this->name); 180 | } 181 | ]; 182 | } 183 | 184 | /** 185 | * Create the instance of the connection type. 186 | * 187 | * @param Closure $pageInfoResolver 188 | * @param Closure $edgeResolver 189 | * @return ObjectType 190 | */ 191 | public function toType(Closure $pageInfoResolver = null, Closure $edgeResolver = null) 192 | { 193 | $this->pageInfoResolver = $pageInfoResolver; 194 | 195 | $this->edgeResolver = $edgeResolver; 196 | 197 | return new ObjectType($this->toArray()); 198 | } 199 | 200 | /** 201 | * Set the type at the end of the connection. 202 | * 203 | * @param Type $type 204 | */ 205 | public function setEdgeType($type) 206 | { 207 | $this->type = $type; 208 | } 209 | 210 | /** 211 | * Set name of connection. 212 | * 213 | * @param string $name 214 | */ 215 | public function setName($name) 216 | { 217 | $this->name = $name; 218 | } 219 | 220 | /** 221 | * Dynamically retrieve the value of an attribute. 222 | * 223 | * @param string $key 224 | * @return mixed 225 | */ 226 | public function __get($key) 227 | { 228 | $attributes = $this->getAttributes(); 229 | 230 | return isset($attributes[$key]) ? $attributes[$key] : null; 231 | } 232 | 233 | /** 234 | * Dynamically check if an attribute is set. 235 | * 236 | * @param string $key 237 | * @return boolean 238 | */ 239 | public function __isset($key) 240 | { 241 | return isset($this->getAttributes()[$key]); 242 | } 243 | 244 | /** 245 | * Get the type of nodes at the end of this connection. 246 | * 247 | * @return mixed 248 | */ 249 | public function type() 250 | { 251 | return null; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/Support/Definition/RelayMutation.php: -------------------------------------------------------------------------------- 1 | ucfirst($this->name()) . 'Payload', 40 | 'fields' => array_merge($this->outputFields(), [ 41 | 'clientMutationId' => [ 42 | 'type' => Type::nonNull(Type::string()), 43 | 'resolve' => function () { 44 | return $this->clientMutationId; 45 | } 46 | ] 47 | ]) 48 | ]); 49 | } 50 | 51 | /** 52 | * Generate Relay compliant arguments. 53 | * 54 | * @return array 55 | */ 56 | public function args() 57 | { 58 | $inputType = new InputObjectType([ 59 | 'name' => ucfirst($this->name()) . 'Input', 60 | 'fields' => array_merge($this->inputFields(), [ 61 | 'clientMutationId' => [ 62 | 'type' => Type::nonNull(Type::string()) 63 | ] 64 | ]) 65 | ]); 66 | 67 | return [ 68 | 'input' => [ 69 | 'type' => Type::nonNull($inputType) 70 | ] 71 | ]; 72 | } 73 | 74 | /** 75 | * Resolve mutation. 76 | * 77 | * @param mixed $_ 78 | * @param array $args 79 | * @param ResolveInfo $info 80 | * @return array 81 | */ 82 | public function resolve($_, $args, ResolveInfo $info) 83 | { 84 | if ($this->mutatesRelayType && isset($args['input']['id'])) { 85 | $args['input']['relay_id'] = $args['input']['id']; 86 | $args['input']['id'] = $this->decodeRelayId($args['input']['id']); 87 | } 88 | 89 | $this->clientMutationId = $args['input']['clientMutationId']; 90 | 91 | return $this->mutateAndGetPayload($args['input'], $info); 92 | } 93 | 94 | /** 95 | * Get input for validation. 96 | * 97 | * @param array $arguments 98 | * @return array 99 | */ 100 | protected function getInput(array $arguments) 101 | { 102 | return array_get($arguments, '1.input', []); 103 | } 104 | 105 | /** 106 | * Perform mutation. 107 | * 108 | * @param array $input 109 | * @param ResolveInfo $info 110 | * @return array 111 | */ 112 | abstract protected function mutateAndGetPayload(array $input, ResolveInfo $info); 113 | 114 | /** 115 | * List of available input fields. 116 | * 117 | * @return array 118 | */ 119 | abstract protected function inputFields(); 120 | 121 | /** 122 | * List of output fields. 123 | * 124 | * @return array 125 | */ 126 | abstract protected function outputFields(); 127 | 128 | /** 129 | * Get name of mutation. 130 | * 131 | * @return string 132 | */ 133 | abstract protected function name(); 134 | } 135 | -------------------------------------------------------------------------------- /src/Support/Definition/RelayType.php: -------------------------------------------------------------------------------- 1 | relayFields(), $this->getConnections(), [ 27 | 'id' => [ 28 | 'type' => Type::nonNull(Type::id()), 29 | 'description' => 'ID of type.', 30 | 'resolve' => function ($obj) { 31 | return $this->encodeGlobalId(get_called_class(), $this->getIdentifier($obj)); 32 | }, 33 | ], 34 | ]); 35 | } 36 | 37 | /** 38 | * Available connections for type. 39 | * 40 | * @return array 41 | */ 42 | protected function connections() 43 | { 44 | return []; 45 | } 46 | 47 | /** 48 | * Generate Relay compliant edges. 49 | * 50 | * @return array 51 | */ 52 | public function getConnections() 53 | { 54 | return collect($this->connections())->transform(function ($edge, $name) { 55 | if (!isset($edge['resolve'])) { 56 | $edge['resolve'] = function ($root, array $args, ResolveInfo $info) use ($name) { 57 | return GraphQL::resolveConnection($root, $args, $info, $name); 58 | }; 59 | } 60 | 61 | $edge['args'] = RelayConnectionType::connectionArgs(); 62 | 63 | return $edge; 64 | 65 | })->toArray(); 66 | } 67 | 68 | /** 69 | * Get the identifier of the type. 70 | * 71 | * @param mixed $obj 72 | * @return mixed 73 | */ 74 | public function getIdentifier($obj) 75 | { 76 | return $obj->getKey(); 77 | } 78 | 79 | /** 80 | * List of available interfaces. 81 | * 82 | * @return array 83 | */ 84 | public function interfaces() 85 | { 86 | return [GraphQL::type('node')]; 87 | } 88 | 89 | /** 90 | * Get list of available fields for type. 91 | * 92 | * @return array 93 | */ 94 | abstract protected function relayFields(); 95 | 96 | /** 97 | * Fetch type data by id. 98 | * 99 | * @param string $id 100 | * 101 | * @return mixed 102 | */ 103 | abstract public function resolveById($id); 104 | } 105 | -------------------------------------------------------------------------------- /src/Support/SchemaGenerator.php: -------------------------------------------------------------------------------- 1 | put($path, $schema); 25 | } 26 | 27 | return $data; 28 | } 29 | 30 | /** 31 | * Put to a file path. 32 | * 33 | * @param string $path 34 | * @param string $contents 35 | * @return mixed 36 | */ 37 | protected function put($path, $contents) 38 | { 39 | $this->makeDirectory(dirname($path)); 40 | 41 | return file_put_contents($path, $contents); 42 | } 43 | 44 | /** 45 | * Make a directory tree recursively. 46 | * 47 | * @param string $dir 48 | * @return void 49 | */ 50 | protected function makeDirectory($dir) 51 | { 52 | if (!is_dir($dir)) { 53 | mkdir($dir, 0777, true); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Support/ValidationError.php: -------------------------------------------------------------------------------- 1 | validator = $validator; 24 | 25 | return $this; 26 | } 27 | 28 | /** 29 | * Get the messages from the validator. 30 | * 31 | * @return array 32 | */ 33 | public function getValidatorMessages() 34 | { 35 | return $this->validator ? $this->validator->messages() : []; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Traits/GlobalIdTrait.php: -------------------------------------------------------------------------------- 1 | decodeGlobalId($id); 39 | 40 | return $id; 41 | } 42 | 43 | /** 44 | * Get the decoded GraphQL Type. 45 | * 46 | * @param string $id 47 | * @return string 48 | */ 49 | public function decodeRelayType($id) 50 | { 51 | list($type, $id) = $this->decodeGlobalId($id); 52 | 53 | return $type; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Traits/MutationTestTrait.php: -------------------------------------------------------------------------------- 1 | generateMutationQuery( 22 | $mutationName, 23 | $this->generateOutputFields($mutationName, $outputFields) 24 | ); 25 | 26 | $variables = ['input' => array_merge(['clientMutationId' => (string)time()], $input)]; 27 | 28 | return $this->post('graphql', [ 29 | 'query' => $query, 30 | 'variables' => $variables 31 | ], $headers); 32 | } 33 | 34 | /** 35 | * Generate mutation query. 36 | * 37 | * @param string $mutation 38 | * @param string $outputFields 39 | * @return string 40 | */ 41 | protected function generateMutationQuery($mutation, $outputFields = '') 42 | { 43 | $mutationName = ucfirst($mutation . 'Mutation'); 44 | $inputName = ucfirst($mutation . 'Input'); 45 | 46 | return 'mutation ' . $mutationName . '($input: ' . $inputName . '!){'. $mutation . '(input: $input){' . $outputFields . '}}'; 47 | } 48 | 49 | /** 50 | * Generate list of available output fields. 51 | * 52 | * @param string $mutationName 53 | * @return string 54 | */ 55 | protected function generateOutputFields($mutationName, array $outputFields) 56 | { 57 | $fields = []; 58 | 59 | if (! empty($outputFields)) { 60 | foreach ($outputFields as $name => $availableFields) { 61 | $fields[] = $name . '{'. implode(',', $availableFields) .'}'; 62 | } 63 | } else { 64 | $fields = $this->availableOutputFields($mutationName); 65 | } 66 | 67 | return implode(',', $fields); 68 | } 69 | 70 | /** 71 | * Get all available output fields for mutation. 72 | * 73 | * @param string $mutationName 74 | * @return array 75 | */ 76 | protected function availableOutputFields($mutationName) 77 | { 78 | $outputFields = ['clientMutationId']; 79 | $mutations = config('relay.schema.mutations'); 80 | $mutation = app($mutations[$mutationName]); 81 | 82 | foreach ($mutation->type()->getFields() as $name => $field) { 83 | if ($field instanceof FieldDefinition) { 84 | $objectType = $field->getType(); 85 | 86 | if ($objectType instanceof ObjectType) { 87 | $fields = $this->includeOutputFields($objectType); 88 | $outputFields[] = $name . '{'. implode(',', $fields) .'}'; 89 | } 90 | } 91 | } 92 | 93 | return $outputFields; 94 | } 95 | 96 | /** 97 | * Determine if output fields should be included. 98 | * 99 | * @param mixed $objectType 100 | * @return boolean 101 | */ 102 | protected function includeOutputFields(ObjectType $objectType) 103 | { 104 | $fields = []; 105 | 106 | foreach ($objectType->getFields() as $name => $field) { 107 | $type = $field->getType(); 108 | 109 | if ($type instanceof ObjectType) { 110 | $config = $type->config; 111 | 112 | if (isset($config['name']) && preg_match('/Connection$/', $config['name'])) { 113 | continue; 114 | } 115 | } 116 | 117 | $fields[] = $name; 118 | } 119 | 120 | return $fields; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Traits/RelayMiddleware.php: -------------------------------------------------------------------------------- 1 | get('query')) { 20 | $relay = app('relay'); 21 | $relay->setupRequest($query); 22 | 23 | foreach ($relay->middleware() as $middleware) { 24 | $this->middleware($middleware); 25 | } 26 | } 27 | } 28 | 29 | /** 30 | * Process GraphQL query. 31 | * 32 | * @param Request $request 33 | * @return Response 34 | */ 35 | public function graphqlQuery(Request $request) 36 | { 37 | $query = $request->get('query'); 38 | $params = $request->get('variables'); 39 | 40 | if (is_string($params)) { 41 | $params = json_decode($params, true); 42 | } 43 | 44 | return app('graphql')->query($query, $params); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Traits/RelayModelTrait.php: -------------------------------------------------------------------------------- 1 | attributes[$this->getKeyName()]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/BaseTest.php: -------------------------------------------------------------------------------- 1 | app['graphql']->query($query, $variables); 18 | 19 | if ($encode) { 20 | return json_encode($response); 21 | } 22 | 23 | return $response; 24 | } 25 | 26 | /** 27 | * Get default service providers. 28 | * 29 | * @param \Illuminate\Foundation\Application $app 30 | * @return array 31 | */ 32 | protected function getPackageProviders($app) 33 | { 34 | return [ 35 | \Nuwave\Relay\LaravelServiceProvider::class, 36 | ]; 37 | } 38 | 39 | /** 40 | * Get list of package aliases. 41 | * 42 | * @param \Illuminate\Foundation\Application $app 43 | * @return array 44 | */ 45 | protected function getPackageAliases($app) 46 | { 47 | return [ 48 | 'GraphQL' => \Nuwave\Relay\Facades\GraphQL::class, 49 | 'Relay' => \Nuwave\Relay\Facades\Relay::class, 50 | ]; 51 | } 52 | 53 | /** 54 | * Define environment setup. 55 | * 56 | * @param \Illuminate\Foundation\Application $app 57 | * @return void 58 | */ 59 | protected function getEnvironmentSetUp($app) 60 | { 61 | $app['config']->set('relay', [ 62 | 'schema' => [ 63 | 'path' => 'schema/schema.php', 64 | 'queries' => [ 65 | 'node' => \Nuwave\Relay\Node\NodeQuery::class, 66 | 'humanByName' => \Nuwave\Relay\Tests\Assets\Queries\HumanByName::class, 67 | ], 68 | 'mutations' => [], 69 | 'types' => [ 70 | 'node' => \Nuwave\Relay\Node\NodeType::class, 71 | 'pageInfo' => \Nuwave\Relay\Support\Definition\PageInfoType::class, 72 | 'human' => \Nuwave\Relay\Tests\Assets\Types\HumanType::class, 73 | ], 74 | ] 75 | ]); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/IntrospectionTest.php: -------------------------------------------------------------------------------- 1 | [ 30 | 'name' => 'Node', 31 | 'kind' => 'INTERFACE', 32 | 'fields' => [[ 33 | 'name' => 'id', 34 | 'type' => [ 35 | 'kind' => 'NON_NULL', 36 | 'ofType' => [ 37 | 'name' => 'ID', 38 | 'kind' => 'SCALAR' 39 | ] 40 | ] 41 | ]] 42 | ] 43 | ]; 44 | 45 | $response = $this->graphqlResponse($query); 46 | 47 | $this->assertEquals($expected, $response['data']); 48 | } 49 | 50 | /** 51 | * @test 52 | */ 53 | public function itAcceptsIntrospectionForNodeQuery() 54 | { 55 | $query = '{ 56 | __schema { 57 | queryType { 58 | fields { 59 | name 60 | type { 61 | name 62 | kind 63 | } 64 | args { 65 | name 66 | type { 67 | kind 68 | ofType { 69 | name 70 | kind 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | }'; 78 | $response = $this->graphqlResponse($query); 79 | $fields = array_get($response, 'data.__schema.queryType.fields'); 80 | 81 | $nodeField = array_first($fields, function ($key, $value) { 82 | return $value['name'] == 'node'; 83 | }); 84 | 85 | $this->assertEquals([ 86 | 'name' => 'node', 87 | 'type' => [ 88 | 'name' => 'Node', 89 | 'kind' => 'INTERFACE' 90 | ], 91 | 'args' => [[ 92 | 'name' => 'id', 93 | 'type' => [ 94 | 'kind' => 'NON_NULL', 95 | 'ofType' => [ 96 | 'name' => 'ID', 97 | 'kind' => 'SCALAR' 98 | ] 99 | ] 100 | ]] 101 | ], $nodeField); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/ObjectIdentificationTest.php: -------------------------------------------------------------------------------- 1 | graphqlResponse($query); 17 | 18 | $this->assertEquals([ 19 | 'id' => $encodedId, 20 | 'name' => 'Luke Skywalker' 21 | ], array_get($response, 'data.node')); 22 | } 23 | 24 | /** 25 | * @test 26 | */ 27 | public function itEncodesIdForResponse() 28 | { 29 | $name = 'Darth Vader'; 30 | $encodedId = base64_encode(HumanType::class . ':' . '1001'); 31 | $query = '{humanByName(name: "'. $name .'"){id, name}}'; 32 | $response = $this->graphqlResponse($query); 33 | 34 | $this->assertEquals([ 35 | 'id' => $encodedId, 36 | 'name' => $name 37 | ], array_get($response, 'data.humanByName')); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/assets/Data/StarWarsData.php: -------------------------------------------------------------------------------- 1 | '1000', 11 | 'name' => 'Luke Skywalker', 12 | 'friends' => ['1002', '1003', '2000', '2001'], 13 | 'appearsIn' => [4, 5, 6], 14 | 'homePlanet' => 'Tatooine', 15 | ]; 16 | } 17 | 18 | private static function vader() 19 | { 20 | return [ 21 | 'id' => '1001', 22 | 'name' => 'Darth Vader', 23 | 'friends' => ['1004'], 24 | 'appearsIn' => [4, 5, 6], 25 | 'homePlanet' => 'Tatooine', 26 | ]; 27 | } 28 | 29 | private static function han() 30 | { 31 | return [ 32 | 'id' => '1002', 33 | 'name' => 'Han Solo', 34 | 'friends' => ['1000', '1003', '2001'], 35 | 'appearsIn' => [4, 5, 6], 36 | ]; 37 | } 38 | 39 | private static function leia() 40 | { 41 | return [ 42 | 'id' => '1003', 43 | 'name' => 'Leia Organa', 44 | 'friends' => ['1000', '1002', '2000', '2001'], 45 | 'appearsIn' => [4, 5, 6], 46 | 'homePlanet' => 'Alderaan', 47 | ]; 48 | } 49 | 50 | private static function tarkin() 51 | { 52 | return [ 53 | 'id' => '1004', 54 | 'name' => 'Wilhuff Tarkin', 55 | 'friends' => ['1001'], 56 | 'appearsIn' => [4], 57 | ]; 58 | } 59 | 60 | public static function humans() 61 | { 62 | return [ 63 | '1000' => self::luke(), 64 | '1001' => self::vader(), 65 | '1002' => self::han(), 66 | '1003' => self::leia(), 67 | '1004' => self::tarkin(), 68 | ]; 69 | } 70 | 71 | private static function threepio() 72 | { 73 | return [ 74 | 'id' => '2000', 75 | 'name' => 'C-3PO', 76 | 'friends' => ['1000', '1002', '1003', '2001'], 77 | 'appearsIn' => [4, 5, 6], 78 | 'primaryFunction' => 'Protocol', 79 | ]; 80 | } 81 | 82 | /** 83 | * We export artoo directly because the schema returns him 84 | * from a root field, and hence needs to reference him. 85 | */ 86 | public static function artoo() 87 | { 88 | return [ 89 | 'id' => '2001', 90 | 'name' => 'R2-D2', 91 | 'friends' => ['1000', '1002', '1003'], 92 | 'appearsIn' => [4, 5, 6], 93 | 'primaryFunction' => 'Astromech', 94 | ]; 95 | } 96 | 97 | public static function droids() 98 | { 99 | return [ 100 | '2000' => self::threepio(), 101 | '2001' => self::artoo(), 102 | ]; 103 | } 104 | 105 | /** 106 | * Helper function to get a character by ID. 107 | */ 108 | public static function getCharacter($id) 109 | { 110 | $humans = self::humans(); 111 | $droids = self::droids(); 112 | if (isset($humans[$id])) { 113 | return $humans[$id]; 114 | } 115 | if (isset($droids[$id])) { 116 | return $droids[$id]; 117 | } 118 | return null; 119 | } 120 | 121 | /** 122 | * @param $episode 123 | * @return array 124 | */ 125 | public static function getHero($episode) 126 | { 127 | if ($episode === 5) { 128 | // Luke is the hero of Episode V. 129 | return self::luke(); 130 | } 131 | // Artoo is the hero otherwise. 132 | return self::artoo(); 133 | } 134 | 135 | /** 136 | * Allows us to query for a character's friends. 137 | */ 138 | public static function getFriends($character) 139 | { 140 | return array_map([__CLASS__, 'getCharacter'], $character['friends']); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /tests/assets/Queries/HumanByName.php: -------------------------------------------------------------------------------- 1 | [ 32 | 'name' => 'name', 33 | 'type' => Type::nonNull(Type::string()) 34 | ] 35 | ]; 36 | } 37 | 38 | /** 39 | * Resolve the query. 40 | * 41 | * @param mixed $_ 42 | * @param array $args 43 | * @param ResolveInfo $info 44 | * @return array 45 | */ 46 | public function resolve($_, array $args, ResolveInfo $info) 47 | { 48 | $humans = StarWarsData::humans(); 49 | 50 | foreach ($humans as $human) { 51 | if ($human['name'] == $args['name']) { 52 | return $human; 53 | } 54 | } 55 | 56 | return null; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/assets/Queries/UpdateHeroNameQuery.php: -------------------------------------------------------------------------------- 1 | [ 29 | 'name' => 'id', 30 | 'type' => Type::nonNull(Type::string()) 31 | ], 32 | 'name' => [ 33 | 'name' => 'name', 34 | 'type' => Type::nonNull(Type::string()) 35 | ] 36 | ]; 37 | } 38 | 39 | /** 40 | * Output fields for mutation. 41 | * 42 | * @return array 43 | */ 44 | protected function outputFields() 45 | { 46 | return [ 47 | 'hero' => [ 48 | 'type' => GraphQL::type('hero'), 49 | 'resolve' => function ($payload) { 50 | 51 | } 52 | ] 53 | ]; 54 | } 55 | 56 | /** 57 | * Mutate payload. 58 | * 59 | * @param array $input 60 | * @param ResolveInfo $info 61 | * @return array 62 | */ 63 | protected function mutateAndGetPayload(array $input, ResolveInfo $info) 64 | { 65 | return ['id' => $input['id']]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/assets/Types/HumanType.php: -------------------------------------------------------------------------------- 1 | 'Human' 13 | ]; 14 | 15 | /** 16 | * Get the identifier of the type. 17 | * 18 | * @param mixed $obj 19 | * @return mixed 20 | */ 21 | public function getIdentifier($obj) 22 | { 23 | return $obj['id']; 24 | } 25 | 26 | /** 27 | * Fetch type data by id. 28 | * 29 | * @param string $id 30 | * @return mixed 31 | */ 32 | public function resolveById($id) 33 | { 34 | return StarWarsData::getCharacter($id); 35 | } 36 | 37 | /** 38 | * Get list of available fields for type. 39 | * 40 | * @return array 41 | */ 42 | protected function relayFields() 43 | { 44 | return [ 45 | 'name' => [ 46 | 'type' => Type::string() 47 | ], 48 | 'homePlanet' => [ 49 | 'type' => Type::string() 50 | ] 51 | ]; 52 | } 53 | 54 | /** 55 | * Available connections for type. 56 | * 57 | * @return array 58 | */ 59 | protected function connections() 60 | { 61 | return []; 62 | } 63 | } 64 | --------------------------------------------------------------------------------