Skip to content

Commit df701da

Browse files
committedOct 4, 2024
feat(laravel): graphql policies
1 parent 4312a1f commit df701da

File tree

14 files changed

+324
-82
lines changed

14 files changed

+324
-82
lines changed
 

‎src/Laravel/ApiPlatformProvider.php

+106-72
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,10 @@ public function register(): void
927927
);
928928
});
929929

930+
if (interface_exists(FieldsBuilderEnumInterface::class)) {
931+
$this->registerGraphQl($this->app);
932+
}
933+
930934
$this->app->singleton(JsonApiObjectNormalizer::class, function (Application $app) {
931935
return new JsonApiObjectNormalizer(
932936
$app->make(ObjectNormalizer::class),
@@ -936,51 +940,6 @@ public function register(): void
936940
);
937941
});
938942

939-
if ($this->app['config']->get('api-platform.graphql.enabled')) {
940-
$this->app->singleton(GraphQlItemNormalizer::class, function (Application $app) {
941-
return new GraphQlItemNormalizer(
942-
$app->make(PropertyNameCollectionFactoryInterface::class),
943-
$app->make(PropertyMetadataFactoryInterface::class),
944-
$app->make(IriConverterInterface::class),
945-
$app->make(IdentifiersExtractorInterface::class),
946-
$app->make(ResourceClassResolverInterface::class),
947-
$app->make(PropertyAccessorInterface::class),
948-
$app->make(NameConverterInterface::class),
949-
$app->make(SerializerClassMetadataFactory::class),
950-
null,
951-
$app->make(ResourceMetadataCollectionFactoryInterface::class),
952-
$app->make(ResourceAccessCheckerInterface::class)
953-
);
954-
});
955-
956-
$this->app->singleton(GraphQlObjectNormalizer::class, function (Application $app) {
957-
return new GraphQlObjectNormalizer(
958-
$app->make(ObjectNormalizer::class),
959-
$app->make(IriConverterInterface::class),
960-
$app->make(IdentifiersExtractorInterface::class),
961-
);
962-
});
963-
}
964-
965-
$this->app->singleton(GraphQlErrorNormalizer::class, function () {
966-
return new GraphQlErrorNormalizer();
967-
});
968-
969-
$this->app->singleton(GraphQlValidationExceptionNormalizer::class, function (Application $app) {
970-
/** @var ConfigRepository */
971-
$config = $app['config'];
972-
973-
return new GraphQlValidationExceptionNormalizer($config->get('api-platform.exception_to_status'));
974-
});
975-
976-
$this->app->singleton(GraphQlHttpExceptionNormalizer::class, function () {
977-
return new GraphQlHttpExceptionNormalizer();
978-
});
979-
980-
$this->app->singleton(GraphQlRuntimeExceptionNormalizer::class, function () {
981-
return new GraphQlHttpExceptionNormalizer();
982-
});
983-
984943
$this->app->bind(SerializerInterface::class, Serializer::class);
985944
$this->app->bind(NormalizerInterface::class, Serializer::class);
986945
$this->app->singleton(Serializer::class, function (Application $app) {
@@ -1009,7 +968,7 @@ public function register(): void
1009968
$list->insert($app->make(JsonApiItemNormalizer::class), -890);
1010969
$list->insert($app->make(JsonApiObjectNormalizer::class), -995);
1011970

1012-
if ($config->get('api-platform.graphql.enabled')) {
971+
if (interface_exists(FieldsBuilderEnumInterface::class)) {
1013972
$list->insert($app->make(GraphQlItemNormalizer::class), -890);
1014973
$list->insert($app->make(GraphQlObjectNormalizer::class), -995);
1015974
$list->insert($app->make(GraphQlErrorNormalizer::class), -790);
@@ -1033,7 +992,8 @@ public function register(): void
1033992
new JsonEncoder('jsonapi'),
1034993
new JsonEncoder('jsonhal'),
1035994
new CsvEncoder(),
1036-
]);
995+
]
996+
);
1037997
});
1038998

1039999
$this->app->singleton(JsonLdItemNormalizer::class, function (Application $app) {
@@ -1078,17 +1038,56 @@ function (Application $app) {
10781038
return new Inflector();
10791039
});
10801040

1081-
if ($this->app['config']->get('api-platform.graphql.enabled')) {
1082-
$this->registerGraphQl($this->app);
1083-
}
1084-
10851041
if ($this->app->runningInConsole()) {
10861042
$this->commands([Console\InstallCommand::class]);
10871043
}
10881044
}
10891045

10901046
private function registerGraphQl(Application $app): void
10911047
{
1048+
$this->app->singleton(GraphQlItemNormalizer::class, function (Application $app) {
1049+
return new GraphQlItemNormalizer(
1050+
$app->make(PropertyNameCollectionFactoryInterface::class),
1051+
$app->make(PropertyMetadataFactoryInterface::class),
1052+
$app->make(IriConverterInterface::class),
1053+
$app->make(IdentifiersExtractorInterface::class),
1054+
$app->make(ResourceClassResolverInterface::class),
1055+
$app->make(PropertyAccessorInterface::class),
1056+
$app->make(NameConverterInterface::class),
1057+
$app->make(SerializerClassMetadataFactory::class),
1058+
null,
1059+
$app->make(ResourceMetadataCollectionFactoryInterface::class),
1060+
$app->make(ResourceAccessCheckerInterface::class)
1061+
);
1062+
});
1063+
1064+
$this->app->singleton(GraphQlObjectNormalizer::class, function (Application $app) {
1065+
return new GraphQlObjectNormalizer(
1066+
$app->make(ObjectNormalizer::class),
1067+
$app->make(IriConverterInterface::class),
1068+
$app->make(IdentifiersExtractorInterface::class),
1069+
);
1070+
});
1071+
1072+
$this->app->singleton(GraphQlErrorNormalizer::class, function () {
1073+
return new GraphQlErrorNormalizer();
1074+
});
1075+
1076+
$this->app->singleton(GraphQlValidationExceptionNormalizer::class, function (Application $app) {
1077+
/** @var ConfigRepository */
1078+
$config = $app['config'];
1079+
1080+
return new GraphQlValidationExceptionNormalizer($config->get('api-platform.exception_to_status'));
1081+
});
1082+
1083+
$this->app->singleton(GraphQlHttpExceptionNormalizer::class, function () {
1084+
return new GraphQlHttpExceptionNormalizer();
1085+
});
1086+
1087+
$this->app->singleton(GraphQlRuntimeExceptionNormalizer::class, function () {
1088+
return new GraphQlHttpExceptionNormalizer();
1089+
});
1090+
10921091
$app->singleton('api_platform.graphql.type_locator', function (Application $app) {
10931092
$tagged = iterator_to_array($app->tagged('api_platform.graphql.type'));
10941093

@@ -1130,44 +1129,78 @@ private function registerGraphQl(Application $app): void
11301129
return new GraphQlSerializerContextBuilder($app->make(NameConverterInterface::class));
11311130
});
11321131

1133-
$app->singleton('api_platform.graphql.state_provider', function (Application $app) {
1132+
$app->singleton(GraphQlReadProvider::class, function (Application $app) {
11341133
/** @var ConfigRepository */
11351134
$config = $app['config'];
1136-
$tagged = iterator_to_array($app->tagged(ParameterProviderInterface::class));
1137-
$resolvers = iterator_to_array($app->tagged('api_platform.graphql.resolver'));
11381135

11391136
return new GraphQlReadProvider(
1140-
new GraphQlDenormalizeProvider(
1141-
new ResolverProvider(
1142-
new ParameterProvider(
1143-
$app->make(CallableProvider::class),
1144-
new ServiceLocator($tagged)
1145-
),
1146-
new ServiceLocator($resolvers),
1147-
),
1148-
$app->make(SerializerInterface::class),
1149-
$app->make(GraphQlSerializerContextBuilder::class)
1150-
),
1137+
$this->app->make(CallableProvider::class),
11511138
$app->make(IriConverterInterface::class),
11521139
$app->make(GraphQlSerializerContextBuilder::class),
11531140
$config->get('api-platform.graphql.nesting_separator') ?? '__'
11541141
);
11551142
});
1143+
$app->alias(GraphQlReadProvider::class, 'api_platform.graphql.state_provider.read');
1144+
1145+
$app->singleton(ResolverProvider::class, function (Application $app) {
1146+
$resolvers = iterator_to_array($app->tagged('api_platform.graphql.resolver'));
1147+
1148+
return new ResolverProvider(
1149+
$app->make(GraphQlReadProvider::class),
1150+
new ServiceLocator($resolvers),
1151+
);
1152+
});
1153+
1154+
$app->alias(ResolverProvider::class, 'api_platform.graphql.state_provider.resolver');
1155+
1156+
$app->singleton(GraphQlDenormalizeProvider::class, function (Application $app) {
1157+
return new GraphQlDenormalizeProvider(
1158+
$this->app->make(ResolverProvider::class),
1159+
$app->make(SerializerInterface::class),
1160+
$app->make(GraphQlSerializerContextBuilder::class)
1161+
);
1162+
});
1163+
1164+
$app->alias(GraphQlDenormalizeProvider::class, 'api_platform.graphql.state_provider.denormalize');
1165+
1166+
$app->singleton('api_platform.graphql.state_provider.parameter', function (Application $app) {
1167+
$tagged = iterator_to_array($app->tagged(ParameterProviderInterface::class));
1168+
$tagged['api_platform.serializer.filter_parameter_provider'] = $app->make(SerializerFilterParameterProvider::class);
1169+
1170+
return new ParameterProvider(
1171+
new ParameterValidatorProvider(
1172+
new SecurityParameterProvider(
1173+
$app->make(GraphQlDenormalizeProvider::class),
1174+
$app->make(ResourceAccessCheckerInterface::class)
1175+
),
1176+
),
1177+
new ServiceLocator($tagged)
1178+
);
1179+
});
1180+
1181+
$app->singleton('api_platform.graphql.state_provider.access_checker', function (Application $app) {
1182+
return new AccessCheckerProvider($app->make('api_platform.graphql.state_provider.parameter'), $app->make(ResourceAccessCheckerInterface::class));
1183+
});
1184+
1185+
$app->singleton(NormalizeProcessor::class, function (Application $app) {
1186+
return new NormalizeProcessor(
1187+
$app->make(SerializerInterface::class),
1188+
$app->make(GraphQlSerializerContextBuilder::class),
1189+
$app->make(Pagination::class)
1190+
);
1191+
});
1192+
$app->alias(NormalizeProcessor::class, 'api_platform.graphql.state_processor.normalize');
11561193

11571194
$app->singleton('api_platform.graphql.state_processor', function (Application $app) {
11581195
return new WriteProcessor(
1159-
new NormalizeProcessor(
1160-
$app->make(SerializerInterface::class),
1161-
$app->make(GraphQlSerializerContextBuilder::class),
1162-
$app->make(Pagination::class)
1163-
),
1196+
$app->make('api_platform.graphql.state_processor.normalize'),
11641197
$app->make(CallableProcessor::class),
11651198
);
11661199
});
11671200

11681201
$app->singleton(ResolverFactoryInterface::class, function (Application $app) {
11691202
return new ResolverFactory(
1170-
$app->make('api_platform.graphql.state_provider'),
1203+
$app->make('api_platform.graphql.state_provider.access_checker'),
11711204
$app->make('api_platform.graphql.state_processor')
11721205
);
11731206
});
@@ -1227,7 +1260,8 @@ private function registerGraphQl(Application $app): void
12271260
$app->make(SerializerInterface::class),
12281261
$app->make(ErrorHandlerInterface::class),
12291262
debug: $config->get('app.debug'),
1230-
negotiator: $app->make(Negotiator::class)
1263+
negotiator: $app->make(Negotiator::class),
1264+
formats: $config->get('api-platform.formats')
12311265
);
12321266
});
12331267
}

‎src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php

+17
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
use ApiPlatform\Metadata\DeleteOperationInterface;
2323
use ApiPlatform\Metadata\Get;
2424
use ApiPlatform\Metadata\GetCollection;
25+
use ApiPlatform\Metadata\GraphQl\DeleteMutation;
26+
use ApiPlatform\Metadata\GraphQl\Mutation;
27+
use ApiPlatform\Metadata\GraphQl\Query;
28+
use ApiPlatform\Metadata\GraphQl\QueryCollection;
29+
use ApiPlatform\Metadata\GraphQl\Subscription;
2530
use ApiPlatform\Metadata\Patch;
2631
use ApiPlatform\Metadata\Post;
2732
use ApiPlatform\Metadata\Put;
@@ -39,6 +44,12 @@ final class EloquentResourceCollectionMetadataFactory implements ResourceMetadat
3944
GetCollection::class => 'viewAny',
4045
Delete::class => 'delete',
4146
Patch::class => 'update',
47+
48+
Query::class => 'view',
49+
QueryCollection::class => 'viewAny',
50+
Mutation::class => 'update',
51+
DeleteMutation::class => 'delete',
52+
Subscription::class => 'viewAny',
4253
];
4354

4455
public function __construct(
@@ -94,6 +105,12 @@ public function create(string $resourceClass): ResourceMetadataCollection
94105
$graphQlOperations = $resourceMetadata->getGraphQlOperations();
95106

96107
foreach ($graphQlOperations ?? [] as $operationName => $graphQlOperation) {
108+
if (!$graphQlOperation->getPolicy() && ($policy = Gate::getPolicyFor($model))) {
109+
if (($policyMethod = self::POLICY_METHODS[$graphQlOperation::class] ?? null) && method_exists($policy, $policyMethod)) {
110+
$graphQlOperation = $graphQlOperation->withPolicy($policyMethod);
111+
}
112+
}
113+
97114
if (!$graphQlOperation->getProvider()) {
98115
$graphQlOperation = $graphQlOperation->withProvider($graphQlOperation instanceof CollectionOperationInterface ? CollectionProvider::class : ItemProvider::class);
99116
}

‎src/Laravel/GraphQl/Controller/EntrypointController.php

+18-6
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ final class EntrypointController
3232
use ContentNegotiationTrait;
3333
private int $debug;
3434

35+
/**
36+
* @param array<string, string[]> $formats
37+
*/
3538
public function __construct(
3639
private readonly SchemaBuilderInterface $schemaBuilder,
3740
private readonly ExecutorInterface $executor,
@@ -40,6 +43,7 @@ public function __construct(
4043
private readonly ErrorHandlerInterface $errorHandler,
4144
bool $debug = false,
4245
?Negotiator $negotiator = null,
46+
private readonly array $formats = [],
4347
) {
4448
$this->debug = $debug ? DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE : DebugFlag::NONE;
4549
$this->negotiator = $negotiator ?? new Negotiator();
@@ -48,14 +52,23 @@ public function __construct(
4852
public function __invoke(Request $request): Response
4953
{
5054
$formats = ['json' => ['application/json'], 'html' => ['text/html']];
55+
56+
foreach ($this->formats as $k => $f) {
57+
if (!isset($formats[$k])) {
58+
$formats[$k] = $f;
59+
}
60+
}
61+
62+
$this->addRequestFormats($request, $formats);
5163
$format = $this->getRequestFormat($request, $formats, false);
64+
$request->setRequestFormat($format);
5265

5366
try {
5467
if ($request->isMethod('GET') && 'html' === $format) {
5568
return ($this->graphiQlAction)();
5669
}
5770

58-
[$query, $operationName, $variables] = $this->parseRequest($request);
71+
[$query, $operationName, $variables] = $this->parseRequest($request, $format);
5972
if (null === $query) {
6073
throw new BadRequestHttpException('GraphQL query is not valid.');
6174
}
@@ -78,7 +91,7 @@ public function __invoke(Request $request): Response
7891
*
7992
* @return array{0: array<string, mixed>|null, 1: string, 2: array<string, mixed>}
8093
*/
81-
private function parseRequest(Request $request): array
94+
private function parseRequest(Request $request, string $format): array
8295
{
8396
$queryParameters = $request->query->all();
8497
$query = $queryParameters['query'] ?? null;
@@ -91,16 +104,15 @@ private function parseRequest(Request $request): array
91104
return [$query, $operationName, $variables];
92105
}
93106

94-
$contentType = method_exists(Request::class, 'getContentTypeFormat') ? $request->getContentTypeFormat() : $request->getContentType();
95-
if ('json' === $contentType) {
107+
if ('json' === $format) {
96108
return $this->parseData($query, $operationName, $variables, $request->getContent());
97109
}
98110

99-
if ('graphql' === $contentType) {
111+
if ('graphql' === $format) {
100112
$query = $request->getContent();
101113
}
102114

103-
if (\in_array($contentType, ['multipart', 'form'], true)) {
115+
if ('multipart' === $format) {
104116
return $this->parseMultipartRequest($query, $operationName, $variables, $request->request->all(), $request->files->all());
105117
}
106118

‎src/Laravel/State/AccessCheckerProvider.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313

1414
namespace ApiPlatform\Laravel\State;
1515

16+
use ApiPlatform\Metadata\HttpOperation;
1617
use ApiPlatform\Metadata\Operation;
1718
use ApiPlatform\Metadata\ResourceAccessCheckerInterface;
1819
use ApiPlatform\State\ProviderInterface;
1920
use Illuminate\Auth\Access\AuthorizationException;
21+
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
2022

2123
/**
2224
* Allows access based on the ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface.
@@ -54,7 +56,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
5456
];
5557

5658
if (!$this->resourceAccessChecker->isGranted($operation->getClass(), $policy, $resourceAccessCheckerContext)) {
57-
throw new AuthorizationException($message ?? 'Access Denied.');
59+
throw $operation instanceof HttpOperation ? new AuthorizationException($message ?? 'Access Denied.') : new AccessDeniedHttpException($message ?? 'Access Denied.');
5860
}
5961

6062
return $body;

‎src/Laravel/Tests/GraphQlAuthTest.php

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Laravel\Tests;
15+
16+
use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait;
17+
use Illuminate\Contracts\Config\Repository;
18+
use Illuminate\Foundation\Application;
19+
use Illuminate\Foundation\Testing\RefreshDatabase;
20+
use Orchestra\Testbench\Attributes\DefineEnvironment;
21+
use Orchestra\Testbench\Concerns\WithWorkbench;
22+
use Orchestra\Testbench\TestCase;
23+
24+
class GraphQlAuthTest extends TestCase
25+
{
26+
use ApiTestAssertionsTrait;
27+
use RefreshDatabase;
28+
use WithWorkbench;
29+
30+
/**
31+
* @param Application $app
32+
*/
33+
protected function defineEnvironment($app): void
34+
{
35+
tap($app['config'], function (Repository $config): void {
36+
$config->set('api-platform.routes.middleware', ['auth:sanctum']);
37+
$config->set('api-platform.graphql.enabled', true);
38+
});
39+
}
40+
41+
public function testUnauthenticated(): void
42+
{
43+
$response = $this->postJson('/api/graphql', [], []);
44+
$response->assertStatus(401);
45+
}
46+
47+
public function testAuthenticated(): void
48+
{
49+
$response = $this->post('/tokens/create');
50+
$token = $response->json()['token'];
51+
$response = $this->get('/api/graphql', ['accept' => ['text/html'], 'authorization' => 'Bearer '.$token]);
52+
$response->assertStatus(200);
53+
$response = $this->postJson('/api/graphql', [
54+
'query' => '{books { edges { node {id, name, publicationDate, author {id, name }}}}}',
55+
], [
56+
'content-type' => 'application/json',
57+
'authorization' => 'Bearer '.$token,
58+
]);
59+
$response->assertStatus(200);
60+
$data = $response->json();
61+
$this->assertArrayHasKey('data', $data);
62+
$this->assertArrayNotHasKey('errors', $data);
63+
}
64+
65+
public function testPolicy(): void
66+
{
67+
$response = $this->post('/tokens/create');
68+
$token = $response->json()['token'];
69+
$response = $this->postJson('/api/graphql', ['query' => 'mutation {
70+
updateVault(input: {secret: "secret", id: "/api/vaults/1"}) {
71+
vault {id}
72+
}
73+
}
74+
'], ['accept' => ['application/json'], 'authorization' => 'Bearer '.$token]);
75+
$response->assertStatus(200);
76+
$data = $response->json();
77+
$this->assertArrayHasKey('errors', $data);
78+
$this->assertEquals('Access Denied.', $data['errors'][0]['message']);
79+
}
80+
81+
/**
82+
* @param Application $app
83+
*/
84+
protected function useProductionMode($app): void
85+
{
86+
tap($app['config'], function (Repository $config): void {
87+
$config->set('api-platform.routes.middleware', ['auth:sanctum']);
88+
$config->set('api-platform.graphql.enabled', true);
89+
$config->set('app.debug', false);
90+
});
91+
}
92+
93+
#[DefineEnvironment('useProductionMode')]
94+
public function testProductionError(): void
95+
{
96+
$response = $this->post('/tokens/create');
97+
$token = $response->json()['token'];
98+
$response = $this->postJson('/api/graphql', ['query' => 'mutation {
99+
updateVault(input: {secret: "secret", id: "/api/vaults/1"}) {
100+
vault {id}
101+
}
102+
}
103+
'], ['accept' => ['application/json'], 'authorization' => 'Bearer '.$token]);
104+
$response->assertStatus(200);
105+
$data = $response->json();
106+
$this->assertArrayHasKey('errors', $data);
107+
$this->assertArrayNotHasKey('trace', $data['errors'][0]);
108+
}
109+
}

‎src/Laravel/Tests/GraphQlTest.php

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Laravel\Tests;
15+
16+
use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait;
17+
use Illuminate\Contracts\Config\Repository;
18+
use Illuminate\Foundation\Application;
19+
use Illuminate\Foundation\Testing\RefreshDatabase;
20+
use Orchestra\Testbench\Concerns\WithWorkbench;
21+
use Orchestra\Testbench\TestCase;
22+
23+
class GraphQlTest extends TestCase
24+
{
25+
use ApiTestAssertionsTrait;
26+
use RefreshDatabase;
27+
use WithWorkbench;
28+
29+
/**
30+
* @param Application $app
31+
*/
32+
protected function defineEnvironment($app): void
33+
{
34+
tap($app['config'], function (Repository $config): void {
35+
$config->set('api-platform.graphql.enabled', true);
36+
});
37+
}
38+
39+
public function testGetBooks(): void
40+
{
41+
$response = $this->postJson('/api/graphql', ['query' => '{books { edges { node {id, name, publicationDate, author {id, name }}}}}'], ['accept' => ['application/json']]);
42+
$response->assertStatus(200);
43+
$data = $response->json();
44+
$this->assertArrayHasKey('data', $data);
45+
$this->assertArrayNotHasKey('errors', $data);
46+
}
47+
}

‎src/Laravel/composer.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
"illuminate/routing": "^11.0",
4848
"illuminate/support": "^11.0",
4949
"illuminate/container": "^11.0",
50-
"laravel/sanctum": "^4.0",
5150
"symfony/web-link": "^6.4 || ^7.1",
5251
"willdurand/negotiation": "^3.1",
5352
"phpstan/phpdoc-parser": "^1.29",
@@ -58,7 +57,8 @@
5857
"larastan/larastan": "^2.0",
5958
"orchestra/testbench": "^9.1",
6059
"phpunit/phpunit": "^11.2",
61-
"api-platform/graphql": "^4.0"
60+
"api-platform/graphql": "^4.0",
61+
"laravel/sanctum": "^4.0"
6262
},
6363
"autoload": {
6464
"psr-4": {

‎src/Laravel/workbench/app/Models/Vault.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use ApiPlatform\Metadata\ApiResource;
1717
use ApiPlatform\Metadata\Delete;
1818
use ApiPlatform\Metadata\GetCollection;
19+
use ApiPlatform\Metadata\GraphQl\Mutation;
1920
use ApiPlatform\Metadata\Post;
2021
use Illuminate\Database\Eloquent\Factories\HasFactory;
2122
use Illuminate\Database\Eloquent\Model;
@@ -33,7 +34,8 @@
3334
write: false
3435
),
3536
new Delete(middleware: 'auth:sanctum', rules: VaultFormRequest::class, provider: [self::class, 'provide']),
36-
]
37+
],
38+
graphQlOperations: [new Mutation(name: 'update', policy: 'update')]
3739
)]
3840
class Vault extends Model
3941
{

‎src/Laravel/workbench/database/factories/VaultFactory.php

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace Workbench\Database\Factories;
1515

1616
use Illuminate\Database\Eloquent\Factories\Factory;
17+
use Illuminate\Support\Str;
1718
use Workbench\App\Models\Vault;
1819

1920
/**

‎src/Laravel/workbench/database/seeders/DatabaseSeeder.php

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Workbench\Database\Factories\PostFactory;
2121
use Workbench\Database\Factories\SluggableFactory;
2222
use Workbench\Database\Factories\UserFactory;
23+
use Workbench\Database\Factories\VaultFactory;
2324
use Workbench\Database\Factories\WithAccessorFactory;
2425

2526
class DatabaseSeeder extends Seeder
@@ -34,5 +35,6 @@ public function run(): void
3435
SluggableFactory::new()->count(10)->create();
3536
UserFactory::new()->create();
3637
WithAccessorFactory::new()->create();
38+
VaultFactory::new()->count(10)->create();
3739
}
3840
}

‎src/Metadata/GraphQl/Operation.php

+4
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ public function __construct(
9090
?OptionsInterface $stateOptions = null,
9191
array|Parameters|null $parameters = null,
9292
?bool $queryParameterValidationEnabled = null,
93+
mixed $rules = null,
94+
?string $policy = null,
9395
array $extraProperties = [],
9496
) {
9597
parent::__construct(
@@ -139,6 +141,8 @@ class: $class,
139141
stateOptions: $stateOptions,
140142
parameters: $parameters,
141143
queryParameterValidationEnabled: $queryParameterValidationEnabled,
144+
rules: $rules,
145+
policy: $policy,
142146
extraProperties: $extraProperties
143147
);
144148
}

‎src/Metadata/GraphQl/Query.php

+4
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ public function __construct(
7373
?OptionsInterface $stateOptions = null,
7474
array|Parameters|null $parameters = null,
7575
?bool $queryParameterValidationEnabled = null,
76+
mixed $rules = null,
77+
?string $policy = null,
7678
array $extraProperties = [],
7779

7880
protected ?bool $nested = null,
@@ -130,6 +132,8 @@ class: $class,
130132
stateOptions: $stateOptions,
131133
parameters: $parameters,
132134
queryParameterValidationEnabled: $queryParameterValidationEnabled,
135+
policy: $policy,
136+
rules: $rules,
133137
extraProperties: $extraProperties
134138
);
135139
}

‎src/Metadata/GraphQl/QueryCollection.php

+4
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ public function __construct(
7474
protected ?OptionsInterface $stateOptions = null,
7575
array|Parameters|null $parameters = null,
7676
?bool $queryParameterValidationEnabled = null,
77+
mixed $rules = null,
78+
?string $policy = null,
7779
array $extraProperties = [],
7880

7981
?bool $nested = null,
@@ -130,6 +132,8 @@ class: $class,
130132
processor: $processor,
131133
parameters: $parameters,
132134
queryParameterValidationEnabled: $queryParameterValidationEnabled,
135+
policy: $policy,
136+
rules: $rules,
133137
extraProperties: $extraProperties,
134138
nested: $nested,
135139
);

‎src/Metadata/GraphQl/Subscription.php

+4
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ public function __construct(
7373
?OptionsInterface $stateOptions = null,
7474
array|Parameters|null $parameters = null,
7575
?bool $queryParameterValidationEnabled = null,
76+
mixed $rules = null,
77+
?string $policy = null,
7678
array $extraProperties = [],
7779
) {
7880
parent::__construct(
@@ -128,6 +130,8 @@ class: $class,
128130
stateOptions: $stateOptions,
129131
parameters: $parameters,
130132
queryParameterValidationEnabled: $queryParameterValidationEnabled,
133+
policy: $policy,
134+
rules: $rules,
131135
extraProperties: $extraProperties,
132136
);
133137
}

0 commit comments

Comments
 (0)
Please sign in to comment.