diff --git a/app/Actions/GraphQL/IndexAction.php b/app/Actions/GraphQL/IndexAction.php index 262d89af8..8552a9a1d 100644 --- a/app/Actions/GraphQL/IndexAction.php +++ b/app/Actions/GraphQL/IndexAction.php @@ -8,7 +8,6 @@ use App\Concerns\Actions\GraphQL\ConstrainsEagerLoads; use App\Concerns\Actions\GraphQL\PaginatesModels; use App\Concerns\Actions\GraphQL\SortsModels; use App\Enums\GraphQL\SortType; -use App\Exceptions\GraphQL\ClientValidationException; use App\GraphQL\Argument\SortArgument; use App\GraphQL\Criteria\Sort\RelationSortCriteria; use App\GraphQL\Criteria\Sort\SortCriteria; @@ -22,6 +21,7 @@ use Illuminate\Contracts\Pagination\Paginator; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Validator; class IndexAction { @@ -57,7 +57,17 @@ class IndexAction $page = Arr::get($args, 'page', 1); $maxCount = Config::get('graphql.pagination_values.max_count'); - throw_if($maxCount !== null && $first > $maxCount, ClientValidationException::class, "Maximum first value is {$maxCount}. Got {$first}. Fetch in smaller chuncks."); + + Validator::make(['first' => $first], [ + 'first' => [ + 'required', 'integer', 'min:1', + function ($attribute, $value, $fail) use ($maxCount, $first): void { + if ($maxCount !== null && $value > $maxCount) { + $fail("You may request at most {$maxCount} items. Got {$first}. Fetch in smaller chuncks."); + } + }, + ], + ])->validate(); $searchBuilder->withPagination($first, $page); diff --git a/app/Concerns/Actions/GraphQL/ConstrainsEagerLoads.php b/app/Concerns/Actions/GraphQL/ConstrainsEagerLoads.php index c4889f6d2..e4ce8fcdf 100644 --- a/app/Concerns/Actions/GraphQL/ConstrainsEagerLoads.php +++ b/app/Concerns/Actions/GraphQL/ConstrainsEagerLoads.php @@ -27,42 +27,37 @@ trait ConstrainsEagerLoads { $resolveInfo = new CustomResolveInfo($resolveInfo); - $fields = Arr::get($resolveInfo->getFieldSelectionWithAliases(100), "{$fieldName}.{$fieldName}.selectionSet") + $selection = Arr::get($resolveInfo->getFieldSelectionWithAliases(100), "{$fieldName}.{$fieldName}.selectionSet") ?? $resolveInfo->getFieldSelectionWithAliases(100); - $this->processEagerLoadForType($query, $fields, $type); + $this->processEagerLoadForType($query, $selection, $type); } /** * Process recursively the relations available for the given type. */ - private function processEagerLoadForType(Builder $builder, array $fields, BaseType|BaseUnion $type): void + private function processEagerLoadForType(Builder $builder, array $selection, BaseType|BaseUnion $type): void { $eagerLoadRelations = []; /** @var array $relations */ $relations = collect($type->relations()) - ->filter(fn (Relation $relation) => Arr::has($fields, $relation->getName())) + ->filter(fn (Relation $relation) => Arr::has($selection, $relation->getName())) ->all(); foreach ($relations as $relation) { $name = $relation->getName(); - $path = $relation->getRelationName(); - $relationSelection = Arr::get($fields, "{$name}.{$name}"); - - $relationArgs = Arr::get($relationSelection, 'args'); + $relationSelection = Arr::get($selection, "{$name}.{$name}"); $relationType = $relation->getBaseType(); - $eagerLoadRelations[$path] = function (EloquentRelation $eloquentRelation) use ($relationSelection, $relationArgs, $relationType, $relation): void { - if ($eloquentRelation instanceof MorphTo) { - $this->processMorphToRelation($relationSelection, $relationType, $eloquentRelation); - } elseif ($relationType instanceof BaseUnion) { - $this->processUnion($relationSelection, $eloquentRelation, $relationType); - } else { - $this->processGenericRelation($eloquentRelation, $relationArgs, $relationSelection, $relationType, $relation); - } + $eagerLoadRelations[$relation->getRelationName()] = function (EloquentRelation $eloquentRelation) use ($relationSelection, $relationType, $relation): void { + match (true) { + $eloquentRelation instanceof MorphTo => $this->processMorphToRelation($relationSelection, $relationType, $eloquentRelation), + $relationType instanceof BaseUnion => $this->processUnion($relationSelection, $relationType, $eloquentRelation), + default => $this->processGenericRelation($relationSelection, $relationType, $eloquentRelation, $relation), + }; }; } @@ -84,7 +79,7 @@ trait ConstrainsEagerLoads $morphConstrains = []; foreach ($types as $type) { - $typeSelection = Arr::get($unions, "{$type->getName()}.selectionSet"); + $typeSelection = Arr::get($unions, "{$type->getName()}.selectionSet", []); $morphConstrains[$type->model()] = function (Builder $query) use ($typeSelection, $type): void { $this->processEagerLoadForType($query, $typeSelection, $type); @@ -97,10 +92,8 @@ trait ConstrainsEagerLoads /** * Process a union relation by applying the eager loads for each type in the union. */ - private function processUnion(array $selection, EloquentRelation $relation, BaseUnion $union): void + private function processUnion(array $selection, BaseUnion $union, EloquentRelation $relation): void { - $builder = $relation->getQuery(); - $unions = Arr::get($selection, 'selectionSet.data.data.unions', []); $types = collect($union->baseTypes()) @@ -110,17 +103,19 @@ trait ConstrainsEagerLoads foreach ($types as $type) { $typeSelection = Arr::get($unions, "{$type->getName()}.selectionSet", []); - $this->processEagerLoadForType($builder, $typeSelection, $type); + $this->processEagerLoadForType($relation->getQuery(), $typeSelection, $type); } } /** * Process a generic relation by applying filters, sorting and eager loads. */ - private function processGenericRelation(EloquentRelation $relation, array $args, array $selection, BaseType $type, Relation $graphqlRelation): void + private function processGenericRelation(array $selection, BaseType $type, EloquentRelation $relation, Relation $graphqlRelation): void { $builder = $relation->getQuery(); + $args = Arr::get($selection, 'args'); + $this->filter($builder, $args, $type); $this->sort($builder, $args, $type, $relation, $graphqlRelation); diff --git a/app/Concerns/Actions/GraphQL/PaginatesModels.php b/app/Concerns/Actions/GraphQL/PaginatesModels.php index 96c5413fa..fd9336a7c 100644 --- a/app/Concerns/Actions/GraphQL/PaginatesModels.php +++ b/app/Concerns/Actions/GraphQL/PaginatesModels.php @@ -4,11 +4,11 @@ declare(strict_types=1); namespace App\Concerns\Actions\GraphQL; -use App\Exceptions\GraphQL\ClientValidationException; use Illuminate\Contracts\Pagination\Paginator; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Validator; trait PaginatesModels { @@ -18,7 +18,17 @@ trait PaginatesModels $page = Arr::get($args, 'page'); $maxCount = Config::get('graphql.pagination_values.max_count'); - throw_if($maxCount !== null && $first > $maxCount, ClientValidationException::class, "Maximum first value is {$maxCount}. Got {$first}. Fetch in smaller chuncks."); + + Validator::make(['first' => $first], [ + 'first' => [ + 'required', 'integer', 'min:1', + function ($attribute, $value, $fail) use ($maxCount, $first): void { + if ($maxCount !== null && $value > $maxCount) { + $fail("You may request at most {$maxCount} items. Got {$first}. Fetch in smaller chuncks."); + } + }, + ], + ])->validate(); return $builder->paginate($first, page: $page); } diff --git a/app/Console/Commands/Database/DatabaseSyncCommand.php b/app/Console/Commands/Database/DatabaseSyncCommand.php index 66e522c70..8e9db864e 100644 --- a/app/Console/Commands/Database/DatabaseSyncCommand.php +++ b/app/Console/Commands/Database/DatabaseSyncCommand.php @@ -44,7 +44,7 @@ class DatabaseSyncCommand extends BaseCommand } if (! $this->option('drop')) { - Schema::withoutForeignKeyConstraints(function () { + Schema::withoutForeignKeyConstraints(function (): void { foreach ([ ...DumpDocumentAction::allowedTables(), ...DumpWikiAction::allowedTables(), diff --git a/app/Exceptions/GraphQL/ClientValidationException.php b/app/Exceptions/GraphQL/ClientValidationException.php deleted file mode 100644 index 4e7efe674..000000000 --- a/app/Exceptions/GraphQL/ClientValidationException.php +++ /dev/null @@ -1,18 +0,0 @@ - $mutation * @return array - * - * @throws ValidationError */ public function validated(array $args, string $mutation): array { @@ -46,11 +42,7 @@ abstract class BaseController $validator = Validator::make($args, $mutationInstance->rulesForValidation($args)); - try { - $validated = $validator->validated(); - } catch (ValidationException $e) { - throw new ValidationError($e->getMessage(), $validator); - } + $validated = $validator->validated(); return [ ...$validated, diff --git a/app/GraphQL/Controllers/User/LikeController.php b/app/GraphQL/Controllers/User/LikeController.php index ce3d66f15..5cd38e2b1 100644 --- a/app/GraphQL/Controllers/User/LikeController.php +++ b/app/GraphQL/Controllers/User/LikeController.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace App\GraphQL\Controllers\User; use App\Contracts\Models\Likeable; -use App\Exceptions\GraphQL\ClientValidationException; use App\GraphQL\Controllers\BaseController; use App\GraphQL\Schema\Mutations\Models\User\LikeMutation; use App\GraphQL\Schema\Mutations\Models\User\UnlikeMutation; use App\Models\User\Like; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Auth; /** @@ -23,41 +23,27 @@ class LikeController extends BaseController /** * @param array $args - * - * @throws ClientValidationException */ - public function store($root, array $args): Model&Likeable + public function store($root, array $args): Model { $validated = $this->validated($args, LikeMutation::class); - foreach ($validated as $likeable) { - if ($likeable instanceof Model && $likeable instanceof Likeable) { - $likeable->like(Auth::user()); + /** @var Model&Likeable $likeable */ + $likeable = Arr::first($validated); - return $likeable; - } - } - - throw new ClientValidationException('One resource is required to like.'); + return $likeable->like(Auth::user()); } /** * @param array $args - * - * @throws ClientValidationException */ - public function destroy($root, array $args): Model&Likeable + public function destroy($root, array $args): Model { $validated = $this->validated($args, UnlikeMutation::class); - foreach ($validated as $likeable) { - if ($likeable instanceof Model && $likeable instanceof Likeable) { - $likeable->unlike(Auth::user()); + /** @var Model&Likeable $likeable */ + $likeable = Arr::first($validated); - return $likeable; - } - } - - throw new ClientValidationException('One resource is required to unlike.'); + return $likeable->unlike(Auth::user()); } } diff --git a/app/GraphQL/Controllers/Wiki/Anime/AnimeYearsController.php b/app/GraphQL/Controllers/Wiki/Anime/AnimeYearsController.php index d09e5d1c8..348af10b7 100644 --- a/app/GraphQL/Controllers/Wiki/Anime/AnimeYearsController.php +++ b/app/GraphQL/Controllers/Wiki/Anime/AnimeYearsController.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace App\GraphQL\Controllers\Wiki\Anime; use App\Actions\GraphQL\IndexAction; -use App\Exceptions\GraphQL\ClientValidationException; use App\GraphQL\Controllers\BaseController; use App\GraphQL\Schema\Fields\Wiki\Anime\AnimeYear\AnimeYearSeason\AnimeYearSeasonSeasonField; use App\GraphQL\Schema\Fields\Wiki\Anime\AnimeYear\AnimeYearSeasonField; @@ -19,6 +18,7 @@ use Illuminate\Contracts\Pagination\Paginator; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Validator; /** * @extends BaseController @@ -35,11 +35,20 @@ class AnimeYearsController extends BaseController $fieldSelection = $resolveInfo->getFieldSelection(1); // Restrict 'animes' field to a unique year. - throw_if( - ($year === null || count($year) > 1) && (Arr::get($fieldSelection, 'season.anime') || Arr::get($fieldSelection, 'seasons.anime')), - ClientValidationException::class, - "Please provide a unique 'year' argument to query the animes field." - ); + Validator::make( + [ + 'year' => $year, + ], + [ + 'year' => [ + function ($attribute, $value, $fail) use ($fieldSelection): void { + if (($value === null || count($value) > 1) && (Arr::get($fieldSelection, 'season.anime') || Arr::get($fieldSelection, 'seasons.anime'))) { + $fail('Exactly one year is required when requesting anime.'); + } + }, + ], + ] + )->validate(); return Anime::query() ->distinct([Anime::ATTRIBUTE_YEAR, Anime::ATTRIBUTE_SEASON]) diff --git a/app/GraphQL/Filter/Filter.php b/app/GraphQL/Filter/Filter.php index 8d9c14bd8..e99e581b5 100644 --- a/app/GraphQL/Filter/Filter.php +++ b/app/GraphQL/Filter/Filter.php @@ -5,12 +5,10 @@ declare(strict_types=1); namespace App\GraphQL\Filter; use App\Enums\Http\Api\Filter\ComparisonOperator; -use App\Exceptions\GraphQL\ClientValidationException; use App\GraphQL\Argument\Argument; use App\GraphQL\Argument\FilterArgument; use GraphQL\Type\Definition\Type; use Illuminate\Support\Facades\Validator; -use Illuminate\Validation\ValidationException; abstract class Filter { @@ -63,14 +61,10 @@ abstract class Filter protected function validateFilterValues(array $filterValues): void { foreach ($filterValues as $filterValue) { - try { - Validator::make( - [$this->fieldName => $filterValue], - [$this->fieldName => $this->getRules()], - )->validate(); - } catch (ValidationException $e) { - throw new ClientValidationException($e->getMessage()); - } + Validator::make( + [$this->fieldName => $filterValue], + [$this->fieldName => $this->getRules()], + )->validate(); } } diff --git a/app/GraphQL/Handler/ErrorHandler.php b/app/GraphQL/Handler/ErrorHandler.php index 4885ef34d..7757a6e55 100644 --- a/app/GraphQL/Handler/ErrorHandler.php +++ b/app/GraphQL/Handler/ErrorHandler.php @@ -6,11 +6,18 @@ namespace App\GraphQL\Handler; use Error as PhpError; use Exception; +use GraphQL\Error\DebugFlag; use GraphQL\Error\Error as GraphQLError; +use GraphQL\Error\FormattedError; use GraphQL\Server\RequestError; use Illuminate\Contracts\Debug\ExceptionHandler; +use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Support\Facades\Config; +use Illuminate\Validation\ValidationException; use Rebing\GraphQL\Error\AuthorizationError; +use Rebing\GraphQL\Error\ProvidesErrorCategory; use Rebing\GraphQL\Error\ValidationError; +use Throwable; class ErrorHandler { @@ -51,4 +58,47 @@ class ErrorHandler return array_map($formatter, $errors); } + + // /** + // * @return array + // * @see ExecutionResult::setErrorFormatter + // */ + public static function formatError(GraphQLError $e): array + { + $debug = Config::get('app.debug') ? (DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE) : DebugFlag::NONE; + $formatter = FormattedError::prepareFormatter(null, $debug); + $error = $formatter($e); + + $previous = $e->getPrevious(); + + if ($previous instanceof Throwable) { + if ($previous instanceof ModelNotFoundException) { + $error['message'] = $previous->getMessage(); + $error['extensions'] = [ + 'category' => 'model_not_found', + 'model' => $previous->getModel(), + ]; + } + + if ($previous instanceof ValidationException) { + $error['message'] = 'validation'; + $error['extensions'] = [ + 'category' => 'validation', + 'validation' => $previous->validator->errors()->getMessages(), + ]; + } + + if ($previous instanceof ValidationError) { + $error['extensions']['validation'] = $previous->getValidatorMessages()->getMessages(); + } + + if ($previous instanceof ProvidesErrorCategory) { + $error['extensions']['category'] = $previous->getCategory(); + } + } elseif ($e instanceof ProvidesErrorCategory) { + $error['extensions']['category'] = $e->getCategory(); + } + + return $error; + } } diff --git a/app/GraphQL/Schema/Fields/User/Like/LikeAnimeThemeEntryField.php b/app/GraphQL/Schema/Fields/User/Like/LikeAnimeThemeEntryField.php index 23d5a3709..587519dd7 100644 --- a/app/GraphQL/Schema/Fields/User/Like/LikeAnimeThemeEntryField.php +++ b/app/GraphQL/Schema/Fields/User/Like/LikeAnimeThemeEntryField.php @@ -48,6 +48,9 @@ class LikeAnimeThemeEntryField extends Field implements BindableField, Creatable { return [ Str::of('prohibits:')->append(LikeController::ATTRIBUTE_PLAYLIST)->__toString(), + 'required_without_all:'.implode(',', [ + LikeController::ATTRIBUTE_PLAYLIST, + ]), ]; } @@ -58,6 +61,9 @@ class LikeAnimeThemeEntryField extends Field implements BindableField, Creatable { return [ Str::of('prohibits:')->append(LikeController::ATTRIBUTE_PLAYLIST)->__toString(), + 'required_without_all:'.implode(',', [ + LikeController::ATTRIBUTE_PLAYLIST, + ]), ]; } } diff --git a/app/GraphQL/Schema/Fields/User/Like/LikePlaylistField.php b/app/GraphQL/Schema/Fields/User/Like/LikePlaylistField.php index a9a762e80..fc98144db 100644 --- a/app/GraphQL/Schema/Fields/User/Like/LikePlaylistField.php +++ b/app/GraphQL/Schema/Fields/User/Like/LikePlaylistField.php @@ -48,6 +48,9 @@ class LikePlaylistField extends Field implements BindableField, CreatableField, { return [ Str::of('prohibits:')->append(LikeController::ATTRIBUTE_ENTRY)->__toString(), + 'required_without_all:'.implode(',', [ + LikeController::ATTRIBUTE_ENTRY, + ]), ]; } @@ -58,6 +61,9 @@ class LikePlaylistField extends Field implements BindableField, CreatableField, { return [ Str::of('prohibits:')->append(LikeController::ATTRIBUTE_ENTRY)->__toString(), + 'required_without_all:'.implode(',', [ + LikeController::ATTRIBUTE_ENTRY, + ]), ]; } } diff --git a/app/GraphQL/Schema/Mutations/Models/User/LikeMutation.php b/app/GraphQL/Schema/Mutations/Models/User/LikeMutation.php index 4d5166012..b20008f64 100644 --- a/app/GraphQL/Schema/Mutations/Models/User/LikeMutation.php +++ b/app/GraphQL/Schema/Mutations/Models/User/LikeMutation.php @@ -10,10 +10,11 @@ use App\GraphQL\Controllers\User\LikeController; use App\GraphQL\Schema\Fields\Field; use App\GraphQL\Schema\Mutations\BaseMutation; use App\GraphQL\Schema\Types\User\LikeType; -use App\GraphQL\Schema\Unions\LikedUnion; +use App\Models\User\Like; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Gate; class LikeMutation extends BaseMutation { @@ -27,6 +28,11 @@ class LikeMutation extends BaseMutation return 'Like a model'; } + public function authorize($root, array $args, $ctx, ?ResolveInfo $resolveInfo = null, $selectFields = null): bool + { + return ($this->response = Gate::inspect('create', [Like::class, ...[]]))->allowed(); + } + /** * Get the arguments for the like mutation. * @@ -56,9 +62,9 @@ class LikeMutation extends BaseMutation /** * The base return type of the mutation. */ - public function baseType(): LikedUnion + public function baseType(): LikeType { - return new LikedUnion(); + return new LikeType(); } public function type(): Type diff --git a/app/GraphQL/Schema/Mutations/Models/User/UnlikeMutation.php b/app/GraphQL/Schema/Mutations/Models/User/UnlikeMutation.php index 912f40c34..2da6f0a65 100644 --- a/app/GraphQL/Schema/Mutations/Models/User/UnlikeMutation.php +++ b/app/GraphQL/Schema/Mutations/Models/User/UnlikeMutation.php @@ -10,10 +10,11 @@ use App\GraphQL\Controllers\User\LikeController; use App\GraphQL\Schema\Fields\Field; use App\GraphQL\Schema\Mutations\BaseMutation; use App\GraphQL\Schema\Types\User\LikeType; -use App\GraphQL\Schema\Unions\LikedUnion; +use App\Models\User\Like; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Gate; class UnlikeMutation extends BaseMutation { @@ -27,6 +28,11 @@ class UnlikeMutation extends BaseMutation return 'Unlike a model'; } + public function authorize($root, array $args, $ctx, ?ResolveInfo $resolveInfo = null, $selectFields = null): bool + { + return ($this->response = Gate::inspect('delete', [Like::class, ...[]]))->allowed(); + } + /** * Get the arguments for the unlike mutation. * @@ -56,9 +62,9 @@ class UnlikeMutation extends BaseMutation /** * The base return type of the mutation. */ - public function baseType(): LikedUnion + public function baseType(): LikeType { - return new LikedUnion(); + return new LikeType(); } public function type(): Type diff --git a/app/GraphQL/Schema/Queries/Admin/CurrentFeaturedThemeQuery.php b/app/GraphQL/Schema/Queries/Admin/CurrentFeaturedThemeQuery.php index 80987d83f..8c9b957a6 100644 --- a/app/GraphQL/Schema/Queries/Admin/CurrentFeaturedThemeQuery.php +++ b/app/GraphQL/Schema/Queries/Admin/CurrentFeaturedThemeQuery.php @@ -8,8 +8,8 @@ use App\GraphQL\Argument\Argument; use App\GraphQL\Schema\Queries\BaseQuery; use App\GraphQL\Schema\Types\Admin\FeaturedThemeType; use App\Models\Admin\FeaturedTheme; -use Closure; use GraphQL\Type\Definition\ResolveInfo; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Date; class CurrentFeaturedThemeQuery extends BaseQuery @@ -48,7 +48,7 @@ class CurrentFeaturedThemeQuery extends BaseQuery * @param array $args * @return FeaturedTheme|null */ - public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields) + public function resolve($root, array $args, $context, ResolveInfo $resolveInfo): ?Model { $builder = FeaturedTheme::query(); diff --git a/app/GraphQL/Schema/Queries/Wiki/FindAnimeByExternalSiteQuery.php b/app/GraphQL/Schema/Queries/Wiki/FindAnimeByExternalSiteQuery.php index 6474c3114..383ebddd4 100644 --- a/app/GraphQL/Schema/Queries/Wiki/FindAnimeByExternalSiteQuery.php +++ b/app/GraphQL/Schema/Queries/Wiki/FindAnimeByExternalSiteQuery.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace App\GraphQL\Schema\Queries\Wiki; use App\Enums\Models\Wiki\ResourceSite; -use App\Exceptions\GraphQL\ClientValidationException; use App\GraphQL\Argument\Argument; use App\GraphQL\Schema\Queries\BaseQuery; use App\GraphQL\Schema\Types\Wiki\AnimeType; @@ -18,6 +17,8 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rules\Enum; use Rebing\GraphQL\Support\Facades\GraphQL; class FindAnimeByExternalSiteQuery extends BaseQuery @@ -71,16 +72,18 @@ class FindAnimeByExternalSiteQuery extends BaseQuery */ public function resolve($root, array $args, $context, ResolveInfo $resolveInfo) { + Validator::make($args, [ + self::ATTRIBUTE_SITE => ['required', new Enum(ResourceSite::class)], + self::ATTRIBUTE_ID => ['required_without:link', 'max:100'], + self::ATTRIBUTE_LINK => ['required_without:id'], + ])->validate(); + $builder = Anime::query(); $site = Arr::get($args, self::ATTRIBUTE_SITE); $externalId = Arr::get($args, self::ATTRIBUTE_ID); $link = Arr::get($args, self::ATTRIBUTE_LINK); - throw_if(is_null($externalId) && is_null($link), ClientValidationException::class, 'At least "id" or "link" is required.'); - - throw_if($externalId !== null && count($externalId) > 100, ClientValidationException::class, 'The "Id" parameter cannot contain more than 100 integer values.'); - $builder->whereRelation(Anime::RELATION_RESOURCES, function (Builder $query) use ($site, $externalId, $link): void { $query->where(ExternalResource::ATTRIBUTE_SITE, $site); diff --git a/app/GraphQL/Schema/Relations/Relation.php b/app/GraphQL/Schema/Relations/Relation.php index 94327e5a7..c79a039bd 100644 --- a/app/GraphQL/Schema/Relations/Relation.php +++ b/app/GraphQL/Schema/Relations/Relation.php @@ -128,7 +128,7 @@ abstract class Relation /** @var Collection $collection */ $collection = $root->{$this->getRelationName()}; - $first = Arr::get($args, 'first'); + $first = max(1, Arr::integer($args, 'first')); $page = Arr::get($args, 'page'); return new LengthAwarePaginator( diff --git a/app/GraphQL/Schema/Types/BaseType.php b/app/GraphQL/Schema/Types/BaseType.php index e8db3350c..3f218c75e 100644 --- a/app/GraphQL/Schema/Types/BaseType.php +++ b/app/GraphQL/Schema/Types/BaseType.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\GraphQL\Schema\Types; use App\Contracts\GraphQL\Fields\DeprecatedField; +use App\Contracts\GraphQL\Fields\DisplayableField; use App\GraphQL\Schema\Fields\Field; use App\GraphQL\Schema\Relations\Relation; use Illuminate\Support\Str; @@ -77,6 +78,7 @@ abstract class BaseType extends RebingType ]); $fields = collect($this->fieldClasses()) + ->filter(fn (Field $field): bool => $field instanceof DisplayableField && $field->canBeDisplayed()) ->mapWithKeys(fn (Field $field): array => [ $field->getName() => [ 'type' => $field->type(), diff --git a/app/Policies/User/LikePolicy.php b/app/Policies/User/LikePolicy.php index 1f1c117f2..b356e41fc 100644 --- a/app/Policies/User/LikePolicy.php +++ b/app/Policies/User/LikePolicy.php @@ -12,6 +12,13 @@ use Illuminate\Auth\Access\Response; class LikePolicy extends BasePolicy { + public function create(User $user): Response + { + return $user->can(CrudPermission::DELETE->format(Like::class)) + ? Response::allow() + : Response::deny(); + } + public function delete(User $user, mixed $value): Response { return $user->can(CrudPermission::DELETE->format(Like::class)) diff --git a/config/graphql.php b/config/graphql.php index 45d96757c..bf21a2545 100644 --- a/config/graphql.php +++ b/config/graphql.php @@ -89,7 +89,7 @@ return [ // 'message' => '', // 'locations' => [] // ] - 'error_formatter' => [Rebing\GraphQL\GraphQL::class, 'formatError'], + 'error_formatter' => [App\GraphQL\Handler\ErrorHandler::class, 'formatError'], /* * Custom Error Handling