diff --git a/config/graphql.php b/config/graphql.php index 132e71f3555..02f8258ce31 100644 --- a/config/graphql.php +++ b/config/graphql.php @@ -42,6 +42,20 @@ // ], + /* + |-------------------------------------------------------------------------- + | Mutations + |-------------------------------------------------------------------------- + | + | Here you may list mutations to be added to the Statamic schema. + | + | https://statamic.dev/graphql#custom-mutations + */ + + 'mutations' => [ + // + ], + /* |-------------------------------------------------------------------------- | Middleware diff --git a/src/GraphQL/DefaultSchema.php b/src/GraphQL/DefaultSchema.php index 6cde23ac0e7..1aaed9f1a17 100644 --- a/src/GraphQL/DefaultSchema.php +++ b/src/GraphQL/DefaultSchema.php @@ -45,7 +45,7 @@ public function getConfig() { return [ 'query' => $this->getQueries(), - 'mutation' => [], + 'mutation' => $this->getMutations(), 'middleware' => $this->getMiddleware(), 'method' => ['GET', 'POST'], ]; @@ -82,4 +82,9 @@ private function getMiddleware() GraphQL::getExtraMiddleware() ); } + + private function getMutations() + { + return config('statamic.graphql.mutations', []); + } } diff --git a/src/GraphQL/Middleware/CacheResponse.php b/src/GraphQL/Middleware/CacheResponse.php index 45b6360f5ab..73b5b146363 100644 --- a/src/GraphQL/Middleware/CacheResponse.php +++ b/src/GraphQL/Middleware/CacheResponse.php @@ -13,6 +13,10 @@ public function handle($request, Closure $next) return $next($request); } + if ($this->isMutation($request)) { + return $next($request); + } + $cache = app(ResponseCache::class); if ($response = $cache->get($request)) { @@ -25,4 +29,11 @@ public function handle($request, Closure $next) return $response; } + + protected function isMutation($request): bool + { + $query = ltrim(strtolower($request->get('query', ''))); + + return str_starts_with($query, 'mutation'); + } } diff --git a/tests/Feature/GraphQL/CustomMutationTest.php b/tests/Feature/GraphQL/CustomMutationTest.php new file mode 100644 index 00000000000..74f51b83a87 --- /dev/null +++ b/tests/Feature/GraphQL/CustomMutationTest.php @@ -0,0 +1,142 @@ +instance('mutation-count', 0); + } + + #[Test] + public function custom_mutation_does_not_yet_exist() + { + $this + ->post('/graphql', ['query' => 'mutation { createItem(name: "test") }']) + ->assertJson(['errors' => [[ + 'message' => 'Schema is not configured for mutations.', + ]]]); + } + + #[Test] + #[DefineEnvironment('addCustomMutationsThroughConfig')] + public function a_custom_mutation_can_be_added_to_the_default_schema_through_config() + { + $this + ->post('/graphql', ['query' => 'mutation { createItem(name: "test") }']) + ->assertGqlOk() + ->assertExactJson(['data' => ['createItem' => 'Item created: test']]); + } + + #[Test] + #[DefineEnvironment('addCustomMutationsThroughConfig')] + public function multiple_custom_mutations_can_be_added() + { + $this + ->post('/graphql', ['query' => 'mutation { createItem(name: "first") }']) + ->assertGqlOk() + ->assertExactJson(['data' => ['createItem' => 'Item created: first']]); + + $this + ->post('/graphql', ['query' => 'mutation { updateItem(id: 1, name: "updated") }']) + ->assertGqlOk() + ->assertExactJson(['data' => ['updateItem' => 'Item 1 updated: updated']]); + } + + #[Test] + #[DefineEnvironment('addCustomMutationsThroughConfig')] + public function mutations_are_not_cached() + { + $this + ->post('/graphql', ['query' => 'mutation { createItem(name: "test") }']) + ->assertGqlOk() + ->assertExactJson(['data' => ['createItem' => 'Item created: test']]); + + $this + ->post('/graphql', ['query' => 'mutation { createItem(name: "test") }']) + ->assertGqlOk() + ->assertExactJson(['data' => ['createItem' => 'Item created: test']]); + + $this->assertEquals(2, app('mutation-count')); + } + + protected function addCustomMutationsThroughConfig($app) + { + $app['config']->set('statamic.graphql.mutations', [ + CreateItemMutation::class, + UpdateItemMutation::class, + ]); + } +} + +class CreateItemMutation extends Mutation +{ + protected $attributes = [ + 'name' => 'createItem', + ]; + + public function type(): Type + { + return GraphQL::string(); + } + + public function args(): array + { + return [ + 'name' => [ + 'type' => Type::nonNull(Type::string()), + 'description' => 'The name of the item to create', + ], + ]; + } + + public function resolve($root, $args) + { + app()->instance('mutation-count', app('mutation-count') + 1); + + return 'Item created: '.$args['name']; + } +} + +class UpdateItemMutation extends Mutation +{ + protected $attributes = [ + 'name' => 'updateItem', + ]; + + public function type(): Type + { + return GraphQL::string(); + } + + public function args(): array + { + return [ + 'id' => [ + 'type' => Type::nonNull(Type::int()), + 'description' => 'The ID of the item to update', + ], + 'name' => [ + 'type' => Type::nonNull(Type::string()), + 'description' => 'The new name of the item', + ], + ]; + } + + public function resolve($root, $args) + { + return "Item {$args['id']} updated: {$args['name']}"; + } +}