From 38f0e2fa76b0fc7e7111b91a3efebdedc5c9b51d Mon Sep 17 00:00:00 2001 From: Kyrch Date: Mon, 12 May 2025 02:07:27 -0300 Subject: [PATCH] feat: GraphQL (#814) --- .env.example | 7 + .env.example-sail | 7 + app/Contracts/Models/HasAggregateViews.php | 23 + app/Enums/Auth/SpecialPermission.php | 2 + app/Enums/Models/Wiki/ThemeType.php | 4 + app/GraphQL/Builders/Admin/FeatureBuilder.php | 33 + .../Builders/Admin/FeaturedThemeBuilder.php | 54 ++ .../List/External/ExternalEntryBuilder.php | 39 ++ .../Builders/List/ExternalProfileBuilder.php | 40 ++ .../List/Playlist/PlaylistTrackBuilder.php | 39 ++ app/GraphQL/Builders/List/PlaylistBuilder.php | 40 ++ .../Directives/LocalizedEnumDirective.php | 50 ++ .../Directives/MiddlewareDirective.php | 41 ++ app/GraphQL/Middleware/GraphQLPolicy.php | 35 + app/GraphQL/Middleware/GraphqlLocalhost.php | 32 + app/GraphQL/Middleware/MaxCount.php | 29 + .../Mutations/List/ExternalProfileMutator.php | 42 ++ .../List/Playlist/PlaylistTrackMutator.php | 124 ++++ .../Mutations/List/PlaylistMutator.php | 127 ++++ .../Policies/Admin/AnnouncementPolicy.php | 14 + app/GraphQL/Policies/Admin/DumpPolicy.php | 14 + app/GraphQL/Policies/Admin/FeaturePolicy.php | 31 + .../Policies/Admin/FeaturedThemePolicy.php | 32 + .../Policies/Auth/PermissionPolicy.php | 85 +++ app/GraphQL/Policies/Auth/RolePolicy.php | 85 +++ app/GraphQL/Policies/Auth/UserPolicy.php | 85 +++ app/GraphQL/Policies/BasePolicy.php | 151 +++++ .../Policies/Discord/DiscordThreadPolicy.php | 14 + app/GraphQL/Policies/Document/PagePolicy.php | 14 + .../List/External/ExternalEntryPolicy.php | 122 ++++ .../Policies/List/ExternalProfilePolicy.php | 105 +++ .../List/Playlist/PlaylistTrackPolicy.php | 123 ++++ app/GraphQL/Policies/List/PlaylistPolicy.php | 106 +++ .../Wiki/Anime/AnimeSynonymPolicy.php | 14 + .../Policies/Wiki/Anime/AnimeThemePolicy.php | 14 + .../Anime/Theme/AnimeThemeEntryPolicy.php | 14 + app/GraphQL/Policies/Wiki/AnimePolicy.php | 14 + app/GraphQL/Policies/Wiki/ArtistPolicy.php | 14 + app/GraphQL/Policies/Wiki/AudioPolicy.php | 14 + .../Policies/Wiki/ExternalResourcePolicy.php | 14 + app/GraphQL/Policies/Wiki/GroupPolicy.php | 14 + app/GraphQL/Policies/Wiki/ImagePolicy.php | 14 + app/GraphQL/Policies/Wiki/SeriesPolicy.php | 14 + .../Policies/Wiki/Song/MembershipPolicy.php | 14 + .../Policies/Wiki/Song/PerformancePolicy.php | 14 + app/GraphQL/Policies/Wiki/SongPolicy.php | 14 + app/GraphQL/Policies/Wiki/StudioPolicy.php | 14 + .../Policies/Wiki/Video/VideoScriptPolicy.php | 14 + app/GraphQL/Policies/Wiki/VideoPolicy.php | 14 + app/GraphQL/Queries/AnimeYear.php | 54 ++ app/GraphQL/Queries/Search.php | 76 +++ app/GraphQL/Resolvers/ExistsResolver.php | 33 + .../Resolvers/ImplodeArrayResolver.php | 29 + app/GraphQL/Resolvers/PivotResolver.php | 36 + app/GraphQL/Resolvers/ViewsCountResolver.php | 34 + .../Schemas/Admin/announcement.graphql | 37 + app/GraphQL/Schemas/Admin/dump.graphql | 44 ++ app/GraphQL/Schemas/Admin/feature.graphql | 39 ++ .../Schemas/Admin/featuredtheme.graphql | 53 ++ app/GraphQL/Schemas/Auth/permission.graphql | 15 + app/GraphQL/Schemas/Auth/role.graphql | 30 + app/GraphQL/Schemas/Auth/user.graphql | 39 ++ app/GraphQL/Schemas/Document/page.graphql | 46 ++ .../List/External/externalentry.graphql | 59 ++ .../List/Playlist/playlisttrack.graphql | 100 +++ .../Schemas/List/externalprofile.graphql | 62 ++ app/GraphQL/Schemas/List/playlist.graphql | 117 ++++ .../Schemas/Pivot/List/playlistimage.graphql | 9 + .../Schemas/Pivot/Wiki/animeimage.graphql | 9 + .../Schemas/Pivot/Wiki/animeresource.graphql | 11 + .../Schemas/Pivot/Wiki/animeseries.graphql | 9 + .../Schemas/Pivot/Wiki/animestudio.graphql | 9 + .../Pivot/Wiki/animethemeentryvideo.graphql | 9 + .../Schemas/Pivot/Wiki/artistimage.graphql | 11 + .../Schemas/Pivot/Wiki/artistmember.graphql | 15 + .../Schemas/Pivot/Wiki/artistresource.graphql | 11 + .../Schemas/Pivot/Wiki/songresource.graphql | 11 + .../Schemas/Pivot/Wiki/studioimage.graphql | 9 + .../Schemas/Pivot/Wiki/studioresource.graphql | 11 + app/GraphQL/Schemas/User/notification.graphql | 19 + .../Wiki/Anime/Theme/animethemeentry.graphql | 68 ++ .../Schemas/Wiki/Anime/animesynonym.graphql | 46 ++ .../Schemas/Wiki/Anime/animetheme.graphql | 51 ++ .../Schemas/Wiki/Song/membership.graphql | 49 ++ .../Schemas/Wiki/Song/performance.graphql | 54 ++ .../Schemas/Wiki/Video/videoscript.graphql | 45 ++ app/GraphQL/Schemas/Wiki/anime.graphql | 104 +++ app/GraphQL/Schemas/Wiki/artist.graphql | 83 +++ app/GraphQL/Schemas/Wiki/audio.graphql | 58 ++ .../Schemas/Wiki/externalresource.graphql | 108 +++ app/GraphQL/Schemas/Wiki/group.graphql | 46 ++ app/GraphQL/Schemas/Wiki/image.graphql | 70 ++ app/GraphQL/Schemas/Wiki/series.graphql | 54 ++ app/GraphQL/Schemas/Wiki/song.graphql | 54 ++ app/GraphQL/Schemas/Wiki/studio.graphql | 56 ++ app/GraphQL/Schemas/Wiki/video.graphql | 101 +++ app/GraphQL/Schemas/schema.graphql | 47 ++ app/GraphQL/Types/Definition/Hidden.php | 15 + app/GraphQL/Types/EnumType.php | 151 +++++ .../Mutation/List/CreatePlaylistValidator.php | 44 ++ .../Playlist/CreatePlaylistTrackValidator.php | 65 ++ .../Playlist/UpdatePlaylistTrackValidator.php | 84 +++ .../Mutation/List/UpdatePlaylistValidator.php | 46 ++ app/Models/List/Playlist.php | 3 +- app/Models/Wiki/Audio.php | 3 +- app/Models/Wiki/Video.php | 3 +- app/Policies/Auth/RolePolicy.php | 4 +- app/Policies/BasePolicy.php | 25 + .../List/Playlist/PlaylistTrackPolicy.php | 4 +- app/Policies/List/PlaylistPolicy.php | 2 +- app/Providers/GraphqlServiceProvider.php | 97 +++ app/Providers/RouteServiceProvider.php | 20 +- composer.json | 3 + composer.lock | 636 +++++++++++++++++- config/app.php | 4 + config/graphiql.php | 66 ++ config/lighthouse.php | 545 +++++++++++++++ .../filament-daterangepicker-filter/.gitkeep | 0 .../date-range-picker.blade.php | 135 ---- 119 files changed, 5923 insertions(+), 145 deletions(-) create mode 100644 app/Contracts/Models/HasAggregateViews.php create mode 100644 app/GraphQL/Builders/Admin/FeatureBuilder.php create mode 100644 app/GraphQL/Builders/Admin/FeaturedThemeBuilder.php create mode 100644 app/GraphQL/Builders/List/External/ExternalEntryBuilder.php create mode 100644 app/GraphQL/Builders/List/ExternalProfileBuilder.php create mode 100644 app/GraphQL/Builders/List/Playlist/PlaylistTrackBuilder.php create mode 100644 app/GraphQL/Builders/List/PlaylistBuilder.php create mode 100644 app/GraphQL/Directives/LocalizedEnumDirective.php create mode 100644 app/GraphQL/Directives/MiddlewareDirective.php create mode 100644 app/GraphQL/Middleware/GraphQLPolicy.php create mode 100644 app/GraphQL/Middleware/GraphqlLocalhost.php create mode 100644 app/GraphQL/Middleware/MaxCount.php create mode 100644 app/GraphQL/Mutations/List/ExternalProfileMutator.php create mode 100644 app/GraphQL/Mutations/List/Playlist/PlaylistTrackMutator.php create mode 100644 app/GraphQL/Mutations/List/PlaylistMutator.php create mode 100644 app/GraphQL/Policies/Admin/AnnouncementPolicy.php create mode 100644 app/GraphQL/Policies/Admin/DumpPolicy.php create mode 100644 app/GraphQL/Policies/Admin/FeaturePolicy.php create mode 100644 app/GraphQL/Policies/Admin/FeaturedThemePolicy.php create mode 100644 app/GraphQL/Policies/Auth/PermissionPolicy.php create mode 100644 app/GraphQL/Policies/Auth/RolePolicy.php create mode 100644 app/GraphQL/Policies/Auth/UserPolicy.php create mode 100644 app/GraphQL/Policies/BasePolicy.php create mode 100644 app/GraphQL/Policies/Discord/DiscordThreadPolicy.php create mode 100644 app/GraphQL/Policies/Document/PagePolicy.php create mode 100644 app/GraphQL/Policies/List/External/ExternalEntryPolicy.php create mode 100644 app/GraphQL/Policies/List/ExternalProfilePolicy.php create mode 100644 app/GraphQL/Policies/List/Playlist/PlaylistTrackPolicy.php create mode 100644 app/GraphQL/Policies/List/PlaylistPolicy.php create mode 100644 app/GraphQL/Policies/Wiki/Anime/AnimeSynonymPolicy.php create mode 100644 app/GraphQL/Policies/Wiki/Anime/AnimeThemePolicy.php create mode 100644 app/GraphQL/Policies/Wiki/Anime/Theme/AnimeThemeEntryPolicy.php create mode 100644 app/GraphQL/Policies/Wiki/AnimePolicy.php create mode 100644 app/GraphQL/Policies/Wiki/ArtistPolicy.php create mode 100644 app/GraphQL/Policies/Wiki/AudioPolicy.php create mode 100644 app/GraphQL/Policies/Wiki/ExternalResourcePolicy.php create mode 100644 app/GraphQL/Policies/Wiki/GroupPolicy.php create mode 100644 app/GraphQL/Policies/Wiki/ImagePolicy.php create mode 100644 app/GraphQL/Policies/Wiki/SeriesPolicy.php create mode 100644 app/GraphQL/Policies/Wiki/Song/MembershipPolicy.php create mode 100644 app/GraphQL/Policies/Wiki/Song/PerformancePolicy.php create mode 100644 app/GraphQL/Policies/Wiki/SongPolicy.php create mode 100644 app/GraphQL/Policies/Wiki/StudioPolicy.php create mode 100644 app/GraphQL/Policies/Wiki/Video/VideoScriptPolicy.php create mode 100644 app/GraphQL/Policies/Wiki/VideoPolicy.php create mode 100644 app/GraphQL/Queries/AnimeYear.php create mode 100644 app/GraphQL/Queries/Search.php create mode 100644 app/GraphQL/Resolvers/ExistsResolver.php create mode 100644 app/GraphQL/Resolvers/ImplodeArrayResolver.php create mode 100644 app/GraphQL/Resolvers/PivotResolver.php create mode 100644 app/GraphQL/Resolvers/ViewsCountResolver.php create mode 100644 app/GraphQL/Schemas/Admin/announcement.graphql create mode 100644 app/GraphQL/Schemas/Admin/dump.graphql create mode 100644 app/GraphQL/Schemas/Admin/feature.graphql create mode 100644 app/GraphQL/Schemas/Admin/featuredtheme.graphql create mode 100644 app/GraphQL/Schemas/Auth/permission.graphql create mode 100644 app/GraphQL/Schemas/Auth/role.graphql create mode 100644 app/GraphQL/Schemas/Auth/user.graphql create mode 100644 app/GraphQL/Schemas/Document/page.graphql create mode 100644 app/GraphQL/Schemas/List/External/externalentry.graphql create mode 100644 app/GraphQL/Schemas/List/Playlist/playlisttrack.graphql create mode 100644 app/GraphQL/Schemas/List/externalprofile.graphql create mode 100644 app/GraphQL/Schemas/List/playlist.graphql create mode 100644 app/GraphQL/Schemas/Pivot/List/playlistimage.graphql create mode 100644 app/GraphQL/Schemas/Pivot/Wiki/animeimage.graphql create mode 100644 app/GraphQL/Schemas/Pivot/Wiki/animeresource.graphql create mode 100644 app/GraphQL/Schemas/Pivot/Wiki/animeseries.graphql create mode 100644 app/GraphQL/Schemas/Pivot/Wiki/animestudio.graphql create mode 100644 app/GraphQL/Schemas/Pivot/Wiki/animethemeentryvideo.graphql create mode 100644 app/GraphQL/Schemas/Pivot/Wiki/artistimage.graphql create mode 100644 app/GraphQL/Schemas/Pivot/Wiki/artistmember.graphql create mode 100644 app/GraphQL/Schemas/Pivot/Wiki/artistresource.graphql create mode 100644 app/GraphQL/Schemas/Pivot/Wiki/songresource.graphql create mode 100644 app/GraphQL/Schemas/Pivot/Wiki/studioimage.graphql create mode 100644 app/GraphQL/Schemas/Pivot/Wiki/studioresource.graphql create mode 100644 app/GraphQL/Schemas/User/notification.graphql create mode 100644 app/GraphQL/Schemas/Wiki/Anime/Theme/animethemeentry.graphql create mode 100644 app/GraphQL/Schemas/Wiki/Anime/animesynonym.graphql create mode 100644 app/GraphQL/Schemas/Wiki/Anime/animetheme.graphql create mode 100644 app/GraphQL/Schemas/Wiki/Song/membership.graphql create mode 100644 app/GraphQL/Schemas/Wiki/Song/performance.graphql create mode 100644 app/GraphQL/Schemas/Wiki/Video/videoscript.graphql create mode 100644 app/GraphQL/Schemas/Wiki/anime.graphql create mode 100644 app/GraphQL/Schemas/Wiki/artist.graphql create mode 100644 app/GraphQL/Schemas/Wiki/audio.graphql create mode 100644 app/GraphQL/Schemas/Wiki/externalresource.graphql create mode 100644 app/GraphQL/Schemas/Wiki/group.graphql create mode 100644 app/GraphQL/Schemas/Wiki/image.graphql create mode 100644 app/GraphQL/Schemas/Wiki/series.graphql create mode 100644 app/GraphQL/Schemas/Wiki/song.graphql create mode 100644 app/GraphQL/Schemas/Wiki/studio.graphql create mode 100644 app/GraphQL/Schemas/Wiki/video.graphql create mode 100644 app/GraphQL/Schemas/schema.graphql create mode 100644 app/GraphQL/Types/Definition/Hidden.php create mode 100644 app/GraphQL/Types/EnumType.php create mode 100644 app/GraphQL/Validators/Mutation/List/CreatePlaylistValidator.php create mode 100644 app/GraphQL/Validators/Mutation/List/Playlist/CreatePlaylistTrackValidator.php create mode 100644 app/GraphQL/Validators/Mutation/List/Playlist/UpdatePlaylistTrackValidator.php create mode 100644 app/GraphQL/Validators/Mutation/List/UpdatePlaylistValidator.php create mode 100644 app/Providers/GraphqlServiceProvider.php create mode 100644 config/graphiql.php create mode 100644 config/lighthouse.php delete mode 100644 resources/views/vendor/filament-daterangepicker-filter/.gitkeep delete mode 100644 resources/views/vendor/filament-daterangepicker-filter/date-range-picker.blade.php diff --git a/.env.example b/.env.example index 04b707040..b695a1540 100644 --- a/.env.example +++ b/.env.example @@ -198,6 +198,13 @@ IMAGE_DISK_ROOT= JETSTREAM_PATH= JETSTREAM_URL=http://localhost +# lighthouse +GRAPHQL_DOMAIN_NAME= +GRAPHQL_PATH=/graphql +LIGHTHOUSE_DEBUG=true +LIGHTHOUSE_MAX_QUERY_COMPLEXITY=100 +LIGHTHOUSE_MAX_QUERY_DEPTH=10 + # logging LOG_CHANNEL=daily LOG_DEPRECATIONS_CHANNEL=null diff --git a/.env.example-sail b/.env.example-sail index 333f986dd..e56f51a2a 100644 --- a/.env.example-sail +++ b/.env.example-sail @@ -196,6 +196,13 @@ IMAGE_DISK_ROOT= JETSTREAM_PATH= JETSTREAM_URL=http://localhost +# lighthouse +GRAPHQL_DOMAIN_NAME= +GRAPHQL_PATH=/graphql +LIGHTHOUSE_DEBUG=true +LIGHTHOUSE_MAX_QUERY_COMPLEXITY=100 +LIGHTHOUSE_MAX_QUERY_DEPTH=10 + # logging LOG_CHANNEL=daily LOG_DEPRECATIONS_CHANNEL=null diff --git a/app/Contracts/Models/HasAggregateViews.php b/app/Contracts/Models/HasAggregateViews.php new file mode 100644 index 000000000..f770ebaaa --- /dev/null +++ b/app/Contracts/Models/HasAggregateViews.php @@ -0,0 +1,23 @@ +where(Feature::ATTRIBUTE_SCOPE, FeatureConstants::NULL_SCOPE); + } +} diff --git a/app/GraphQL/Builders/Admin/FeaturedThemeBuilder.php b/app/GraphQL/Builders/Admin/FeaturedThemeBuilder.php new file mode 100644 index 000000000..11d54bed6 --- /dev/null +++ b/app/GraphQL/Builders/Admin/FeaturedThemeBuilder.php @@ -0,0 +1,54 @@ +whereNotNull(FeaturedTheme::ATTRIBUTE_START_AT) + ->whereDate(FeaturedTheme::ATTRIBUTE_START_AT, ComparisonOperator::LTE->value, Date::now()); + } + + /** + * Apply the query builder to the index query. + * + * @param Builder $builder + * @param mixed $value + * @param mixed $root + * @param array $args + * @param GraphQLContext $context + * @param ResolveInfo $resolveInfo + * @return Builder + */ + public function current(Builder $builder, mixed $value, mixed $root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): Builder + { + return $builder->whereNotNull(FeaturedTheme::ATTRIBUTE_START_AT) + ->whereNotNull(FeaturedTheme::ATTRIBUTE_END_AT) + ->whereDate(FeaturedTheme::ATTRIBUTE_START_AT, ComparisonOperator::LTE->value, Date::now()) + ->whereDate(FeaturedTheme::ATTRIBUTE_END_AT, ComparisonOperator::GT->value, Date::now()); + } +} diff --git a/app/GraphQL/Builders/List/External/ExternalEntryBuilder.php b/app/GraphQL/Builders/List/External/ExternalEntryBuilder.php new file mode 100644 index 000000000..278045e92 --- /dev/null +++ b/app/GraphQL/Builders/List/External/ExternalEntryBuilder.php @@ -0,0 +1,39 @@ +where(ExternalEntry::ATTRIBUTE_PROFILE, $profile->getKey()); + + return $builder; + } +} diff --git a/app/GraphQL/Builders/List/ExternalProfileBuilder.php b/app/GraphQL/Builders/List/ExternalProfileBuilder.php new file mode 100644 index 000000000..8fd5f88d4 --- /dev/null +++ b/app/GraphQL/Builders/List/ExternalProfileBuilder.php @@ -0,0 +1,40 @@ +where(ExternalProfile::ATTRIBUTE_VISIBILITY, ExternalProfileVisibility::PUBLIC->value); + + if ($user = Auth::user()) { + return $builder->orWhereBelongsTo($user, ExternalProfile::RELATION_USER); + } + + return $builder; + } +} diff --git a/app/GraphQL/Builders/List/Playlist/PlaylistTrackBuilder.php b/app/GraphQL/Builders/List/Playlist/PlaylistTrackBuilder.php new file mode 100644 index 000000000..265fb7db0 --- /dev/null +++ b/app/GraphQL/Builders/List/Playlist/PlaylistTrackBuilder.php @@ -0,0 +1,39 @@ +where(PlaylistTrack::ATTRIBUTE_PLAYLIST, $playlist->getKey()); + + return $builder; + } +} diff --git a/app/GraphQL/Builders/List/PlaylistBuilder.php b/app/GraphQL/Builders/List/PlaylistBuilder.php new file mode 100644 index 000000000..d91e11f33 --- /dev/null +++ b/app/GraphQL/Builders/List/PlaylistBuilder.php @@ -0,0 +1,40 @@ +where(Playlist::ATTRIBUTE_VISIBILITY, PlaylistVisibility::PUBLIC->value); + + if ($user = Auth::user()) { + return $builder->orWhereBelongsTo($user, Playlist::RELATION_USER); + } + + return $builder; + } +} diff --git a/app/GraphQL/Directives/LocalizedEnumDirective.php b/app/GraphQL/Directives/LocalizedEnumDirective.php new file mode 100644 index 000000000..80cac8ed2 --- /dev/null +++ b/app/GraphQL/Directives/LocalizedEnumDirective.php @@ -0,0 +1,50 @@ +wrapResolver( + fn (callable $resolver) => function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($resolver) { + + /** @phpstan-ignore-next-line */ + $enum = $resolver($root, $args, $context, $resolveInfo); + + return $enum->localize(); + }); + } +} diff --git a/app/GraphQL/Directives/MiddlewareDirective.php b/app/GraphQL/Directives/MiddlewareDirective.php new file mode 100644 index 000000000..df9722939 --- /dev/null +++ b/app/GraphQL/Directives/MiddlewareDirective.php @@ -0,0 +1,41 @@ +directiveArgValue('class'); + + App::make($class)->handle(request(), fn () => null); + } +} diff --git a/app/GraphQL/Middleware/GraphQLPolicy.php b/app/GraphQL/Middleware/GraphQLPolicy.php new file mode 100644 index 000000000..946e0b4bc --- /dev/null +++ b/app/GraphQL/Middleware/GraphQLPolicy.php @@ -0,0 +1,35 @@ + Str::of($modelClass) + ->replace('Models', 'GraphQL\\Policies') + ->append('Policy') + ->__toString() + ); + + return $next($request); + } +} diff --git a/app/GraphQL/Middleware/GraphqlLocalhost.php b/app/GraphQL/Middleware/GraphqlLocalhost.php new file mode 100644 index 000000000..b6d3c4e57 --- /dev/null +++ b/app/GraphQL/Middleware/GraphqlLocalhost.php @@ -0,0 +1,32 @@ +ip(); + + if ($ip !== '127.0.0.1' && app()->isProduction()) { + abort(403, "GraphQL is only enabled for localhost"); + } + + return $next($request); + } +} diff --git a/app/GraphQL/Middleware/MaxCount.php b/app/GraphQL/Middleware/MaxCount.php new file mode 100644 index 000000000..3fe2c9ccb --- /dev/null +++ b/app/GraphQL/Middleware/MaxCount.php @@ -0,0 +1,29 @@ +ip() === '127.0.0.1' ? null : 100); + + return $next($request); + } +} diff --git a/app/GraphQL/Mutations/List/ExternalProfileMutator.php b/app/GraphQL/Mutations/List/ExternalProfileMutator.php new file mode 100644 index 000000000..56ecf1952 --- /dev/null +++ b/app/GraphQL/Mutations/List/ExternalProfileMutator.php @@ -0,0 +1,42 @@ +canBeSynced()) { + return new JsonResponse([ + 'error' => 'This external profile cannot be synced at the moment.' + ], 403); + } + + $profile->startSyncJob(); + + return new JsonResponse([ + 'message' => 'Job dispatched.' + ], 201); + } +} diff --git a/app/GraphQL/Mutations/List/Playlist/PlaylistTrackMutator.php b/app/GraphQL/Mutations/List/Playlist/PlaylistTrackMutator.php new file mode 100644 index 000000000..8c60eb4a1 --- /dev/null +++ b/app/GraphQL/Mutations/List/Playlist/PlaylistTrackMutator.php @@ -0,0 +1,124 @@ +store($playlist, PlaylistTrack::query(), $args); + + return $stored; + } + + /** + * Update the specified resource. + * + * @param null $_ + * @param array $args + * @return PlaylistTrack + */ + public function update($_, array $args): PlaylistTrack + { + /** @var PlaylistTrack $track */ + $track = Arr::pull($args, self::ROUTE_SLUG); + + $action = new UpdateTrackAction(); + + /** @var PlaylistTrack $updated */ + $updated = $action->update($track->playlist, $track, $args); + + return $updated; + } + + /** + * Remove the specified resource. + * + * @param null $_ + * @param array $args + * @return PlaylistTrack + */ + public function destroy($_, array $args): PlaylistTrack + { + /** @var PlaylistTrack $track */ + $track = Arr::get($args, self::ROUTE_SLUG); + + $action = new DestroyTrackAction(); + + /** @var PlaylistTrack $destroyed */ + $destroyed = $action->destroy($track->playlist, $track); + + return $destroyed; + } + + /** + * Restore the specified resource. + * + * @param null $_ + * @param array $args + * @return PlaylistTrack + */ + public function restore($_, array $args): PlaylistTrack + { + /** @var PlaylistTrack $track */ + $track = Arr::get($args, self::ROUTE_SLUG); + + $action = new RestoreTrackAction(); + + /** @var PlaylistTrack $restored */ + $restored = $action->restore($track->playlist, $track); + + return $restored; + } + + /** + * Hard-delete the specified resource. + * + * @param null $_ + * @param array $args + * @return JsonResponse + */ + public function forceDelete($_, array $args): JsonResponse + { + /** @var PlaylistTrack $track */ + $track = Arr::get($args, self::ROUTE_SLUG); + + $action = new ForceDeleteTrackAction(); + + $message = $action->forceDelete($track->playlist, $track); + + return new JsonResponse([ + 'message' => $message, + ]); + } +} diff --git a/app/GraphQL/Mutations/List/PlaylistMutator.php b/app/GraphQL/Mutations/List/PlaylistMutator.php new file mode 100644 index 000000000..919d88e64 --- /dev/null +++ b/app/GraphQL/Mutations/List/PlaylistMutator.php @@ -0,0 +1,127 @@ + Auth::id() + ]; + + $action = new StoreAction(); + + /** @var Playlist $stored */ + $stored = $action->store(Playlist::query(), $parameters); + + return $stored; + } + + /** + * Update the specified resource. + * + * @param null $_ + * @param array $args + * @return Playlist + */ + public function update($_, array $args): Playlist + { + /** @var Playlist $playlist */ + $playlist = Arr::pull($args, self::ROUTE_SLUG); + + $action = new UpdateAction(); + + /** @var Playlist $updated */ + $updated = $action->update($playlist, $args); + + return $updated; + } + + /** + * Remove the specified resource. + * + * @param null $_ + * @param array $args + * @return Playlist + */ + public function destroy($_, array $args): Playlist + { + /** @var Playlist $playlist */ + $playlist = Arr::get($args, self::ROUTE_SLUG); + + $action = new DestroyAction(); + + /** @var Playlist $destroyed */ + $destroyed = $action->destroy($playlist); + + return $destroyed; + } + + /** + * Restore the specified resource. + * + * @param null $_ + * @param array $args + * @return Playlist + */ + public function restore($_, array $args): Playlist + { + $hashId = Arr::get($args, self::ROUTE_SLUG); + + $playlist = Playlist::withTrashed()->firstWhere(Playlist::ATTRIBUTE_HASHID, $hashId); + + $action = new RestoreAction(); + + /** @var Playlist $restored */ + $restored = $action->restore($playlist); + + return $restored; + } + + /** + * Hard-delete the specified resource. + * + * @param null $_ + * @param array $args + * @return JsonResponse + */ + public function forceDelete($_, array $args): JsonResponse + { + /** @var Playlist $playlist */ + $playlist = Arr::get($args, self::ROUTE_SLUG); + + $action = new ForceDeleteAction(); + + $message = $action->forceDelete($playlist); + + return new JsonResponse([ + 'message' => $message, + ]); + } +} diff --git a/app/GraphQL/Policies/Admin/AnnouncementPolicy.php b/app/GraphQL/Policies/Admin/AnnouncementPolicy.php new file mode 100644 index 000000000..b0dd4ddd7 --- /dev/null +++ b/app/GraphQL/Policies/Admin/AnnouncementPolicy.php @@ -0,0 +1,14 @@ +isNullScope(); + } +} diff --git a/app/GraphQL/Policies/Admin/FeaturedThemePolicy.php b/app/GraphQL/Policies/Admin/FeaturedThemePolicy.php new file mode 100644 index 000000000..7d1b3339f --- /dev/null +++ b/app/GraphQL/Policies/Admin/FeaturedThemePolicy.php @@ -0,0 +1,32 @@ +start_at->isBefore(Date::now()); + } +} diff --git a/app/GraphQL/Policies/Auth/PermissionPolicy.php b/app/GraphQL/Policies/Auth/PermissionPolicy.php new file mode 100644 index 000000000..0154a4ddf --- /dev/null +++ b/app/GraphQL/Policies/Auth/PermissionPolicy.php @@ -0,0 +1,85 @@ +can(CrudPermission::VIEW->format(Permission::class)); + } + + /** + * Determine whether the user can view the model. + * + * @param User|null $user + * @param array|null $injected + * @return bool + * + * @noinspection PhpUnusedParameterInspection + */ + public function view(?User $user, ?array $injected = null): bool + { + return $user !== null && $user->can(CrudPermission::VIEW->format(Permission::class)); + } + + /** + * Determine whether the user can update the model. + * + * @param User $user + * @param array $injected + * @return bool + * + * @noinspection PhpUnusedParameterInspection + */ + public function update(User $user, array $injected): bool + { + return $user->can(CrudPermission::UPDATE->format(Permission::class)); + } + + /** + * Determine whether the user can delete the model. + * + * @param User $user + * @param array $injected + * @return bool + * + * @noinspection PhpUnusedParameterInspection + */ + public function delete(User $user, array $injected): bool + { + return $user->can(CrudPermission::DELETE->format(Permission::class)); + } + + /** + * Determine whether the user can restore the model. + * + * @param User $user + * @param array $injected + * @return bool + * + * @noinspection PhpUnusedParameterInspection + */ + public function restore(User $user, array $injected): bool + { + return $user->can(ExtendedCrudPermission::RESTORE->format(Permission::class)); + } +} diff --git a/app/GraphQL/Policies/Auth/RolePolicy.php b/app/GraphQL/Policies/Auth/RolePolicy.php new file mode 100644 index 000000000..8108d0ffc --- /dev/null +++ b/app/GraphQL/Policies/Auth/RolePolicy.php @@ -0,0 +1,85 @@ +can(CrudPermission::VIEW->format(Role::class)); + } + + /** + * Determine whether the user can view the model. + * + * @param User|null $user + * @param array|null $injected + * @return bool + * + * @noinspection PhpUnusedParameterInspection + */ + public function view(?User $user, ?array $injected = null): bool + { + return $user !== null && $user->can(CrudPermission::VIEW->format(Role::class)); + } + + /** + * Determine whether the user can update the model. + * + * @param User $user + * @param array $injected + * @return bool + * + * @noinspection PhpUnusedParameterInspection + */ + public function update(User $user, array $injected): bool + { + return $user->can(CrudPermission::UPDATE->format(Role::class)); + } + + /** + * Determine whether the user can delete the model. + * + * @param User $user + * @param array $injected + * @return bool + * + * @noinspection PhpUnusedParameterInspection + */ + public function delete(User $user, array $injected): bool + { + return $user->can(CrudPermission::DELETE->format(Role::class)); + } + + /** + * Determine whether the user can restore the model. + * + * @param User $user + * @param array $injected + * @return bool + * + * @noinspection PhpUnusedParameterInspection + */ + public function restore(User $user, array $injected): bool + { + return $user->can(ExtendedCrudPermission::RESTORE->format(Role::class)); + } +} diff --git a/app/GraphQL/Policies/Auth/UserPolicy.php b/app/GraphQL/Policies/Auth/UserPolicy.php new file mode 100644 index 000000000..81f9c070b --- /dev/null +++ b/app/GraphQL/Policies/Auth/UserPolicy.php @@ -0,0 +1,85 @@ +can(CrudPermission::VIEW->format(User::class)); + } + + /** + * Determine whether the user can view the model. + * + * @param User|null $user + * @param array|null $injected + * @return bool + * + * @noinspection PhpUnusedParameterInspection + */ + public function view(?User $user, ?array $injected = null): bool + { + return $user !== null && $user->can(CrudPermission::VIEW->format(User::class)); + } + + /** + * Determine whether the user can update the model. + * + * @param User $user + * @param array $injected + * @return bool + * + * @noinspection PhpUnusedParameterInspection + */ + public function update(User $user, array $injected): bool + { + return $user->can(CrudPermission::UPDATE->format(User::class)); + } + + /** + * Determine whether the user can delete the model. + * + * @param User $user + * @param array $injected + * @return bool + * + * @noinspection PhpUnusedParameterInspection + */ + public function delete(User $user, array $injected): bool + { + return $user->can(CrudPermission::DELETE->format(User::class)); + } + + /** + * Determine whether the user can restore the model. + * + * @param User $user + * @param array $injected + * @return bool + * + * @noinspection PhpUnusedParameterInspection + */ + public function restore(User $user, array $injected): bool + { + return $user->can(ExtendedCrudPermission::RESTORE->format(User::class)); + } +} diff --git a/app/GraphQL/Policies/BasePolicy.php b/app/GraphQL/Policies/BasePolicy.php new file mode 100644 index 000000000..bd914149e --- /dev/null +++ b/app/GraphQL/Policies/BasePolicy.php @@ -0,0 +1,151 @@ +replace('GraphQL\\Policies', 'Models') + ->remove('Policy') + ->__toString(); + } + + /** + * Determine whether the user can view any models. + * + * @param User|null $user + * @param array|null $injected + * @return bool + */ + public function viewAny(?User $user, ?array $injected = null): bool + { + return true; + } + + /** + * Determine whether the user can view the model. + * + * @param User|null $user + * @param array|null $injected + * @return bool + */ + public function view(?User $user, ?array $injected = null): bool + { + return true; + } + + /** + * Determine whether the user can create models. + * + * @param User $user + * @param array|null $injected + * @return bool + */ + public function create(User $user, ?array $injected = null): bool + { + return $user->can(CrudPermission::CREATE->format(static::getModel())); + } + + /** + * Determine whether the user can update the model. + * + * @param User $user + * @param array $injected + * @return bool + */ + public function update(User $user, array $injected): bool + { + /** @var BaseModel $model */ + $model = Arr::get($injected, 'id'); + + return (!($model instanceof BaseModel) || !$model->trashed()) && $user->can(CrudPermission::UPDATE->format(static::getModel())); + } + + /** + * Determine whether the user can delete the model. + * + * @param User $user + * @param array $injected + * @return bool + */ + public function delete(User $user, array $injected): bool + { + /** @var BaseModel $model */ + $model = Arr::get($injected, 'id'); + + return (!($model instanceof BaseModel) || !$model->trashed()) && $user->can(CrudPermission::DELETE->format(static::getModel())); + } + + /** + * Determine whether the user can permanently delete the model. + * + * @param User $user + * @return bool + */ + public function forceDelete(User $user): bool + { + return $user->can(ExtendedCrudPermission::FORCE_DELETE->format(static::getModel())); + } + + /** + * Determine whether the user can permanently delete any model. + * + * @param User $user + * @return bool + */ + public function forceDeleteAny(User $user): bool + { + return $user->can(ExtendedCrudPermission::FORCE_DELETE->format(static::getModel())); + } + + /** + * Determine whether the user can restore the model. + * + * @param User $user + * @param array $injected + * @return bool + */ + public function restore(User $user, array $injected): bool + { + /** @var BaseModel $model */ + $model = Arr::get($injected, 'id'); + + return (!($model instanceof BaseModel) || $model->trashed()) && $user->can(ExtendedCrudPermission::RESTORE->format(static::getModel())); + } + + /** + * Determine whether the user can restore any model. + * + * @param User $user + * @return bool + */ + public function restoreAny(User $user): bool + { + return $user->can(ExtendedCrudPermission::RESTORE->format(static::getModel())); + } +} diff --git a/app/GraphQL/Policies/Discord/DiscordThreadPolicy.php b/app/GraphQL/Policies/Discord/DiscordThreadPolicy.php new file mode 100644 index 000000000..3c3572a5b --- /dev/null +++ b/app/GraphQL/Policies/Discord/DiscordThreadPolicy.php @@ -0,0 +1,14 @@ +user()->is($user) || ExternalProfileVisibility::PRIVATE !== $profile?->visibility) && $user->can(CrudPermission::VIEW->format(ExternalEntry::class)) + : ExternalProfileVisibility::PRIVATE !== $profile?->visibility; + } + + /** + * Determine whether the user can view the model. + * + * @param User|null $user + * @param array|null $injected + * @return bool + * + * @noinspection PhpUnusedParameterInspection + */ + public function view(?User $user, ?array $injected = null): bool + { + /** @var ExternalProfile|null $profile */ + $profile = Arr::get($injected, 'profile'); + + return $user !== null + ? ($profile?->user()->is($user) || ExternalProfileVisibility::PRIVATE !== $profile?->visibility) && $user->can(CrudPermission::VIEW->format(ExternalEntry::class)) + : ExternalProfileVisibility::PRIVATE !== $profile?->visibility; + } + + /** + * Determine whether the user can create models. + * + * @param User $user + * @param array|null $injected + * @return bool + */ + public function create(User $user, ?array $injected = null): bool + { + /** @var ExternalProfile|null $profile */ + $profile = Arr::get($injected, 'profile'); + + return $profile?->user()->is($user); + } + + /** + * Determine whether the user can update the model. + * + * @param User $user + * @param array $injected + * @return bool + */ + public function update(User $user, array $injected): bool + { + /** @var ExternalProfile|null $profile */ + $profile = Arr::get($injected, 'profile'); + /** @var ExternalEntry $entry */ + $entry = Arr::get($injected, 'id'); + + return !$entry->trashed() && $profile?->user()->is($user) && $user->can(CrudPermission::UPDATE->format(ExternalEntry::class)); + } + + /** + * Determine whether the user can delete the model. + * + * @param User $user + * @param array $injected + * @return bool + */ + public function delete(User $user, array $injected): bool + { + /** @var ExternalProfile|null $profile */ + $profile = Arr::get($injected, 'profile'); + /** @var ExternalEntry $entry */ + $entry = Arr::get($injected, 'id'); + + return !$entry->trashed() && $profile?->user()->is($user) && $user->can(CrudPermission::DELETE->format(ExternalEntry::class)); + } + + /** + * Determine whether the user can restore the model. + * + * @param User $user + * @param array $injected + * @return bool + */ + public function restore(User $user, array $injected): bool + { + /** @var ExternalProfile|null $profile */ + $profile = Arr::get($injected, 'profile'); + /** @var ExternalEntry $entry */ + $entry = Arr::get($injected, 'id'); + + return $entry->trashed() && $profile?->user()->is($user) && $user->can(ExtendedCrudPermission::RESTORE->format(ExternalEntry::class)); + } +} diff --git a/app/GraphQL/Policies/List/ExternalProfilePolicy.php b/app/GraphQL/Policies/List/ExternalProfilePolicy.php new file mode 100644 index 000000000..2d33caafd --- /dev/null +++ b/app/GraphQL/Policies/List/ExternalProfilePolicy.php @@ -0,0 +1,105 @@ +can(CrudPermission::VIEW->format(ExternalProfile::class)); + } + + /** + * Determine whether the user can view the model. + * + * @param User|null $user + * @param array|null $injected + * @return bool + */ + public function view(?User $user, ?array $injected = null): bool + { + /** @var ExternalProfile $profile */ + $profile = Arr::get($injected, 'id'); + + return $user !== null + ? ($profile->user()->is($user) || ExternalProfileVisibility::PRIVATE !== $profile->visibility) && $user->can(CrudPermission::VIEW->format(ExternalProfile::class)) + : ExternalProfileVisibility::PRIVATE !== $profile->visibility; + } + + /** + * Determine whether the user can create models. + * + * @param User $user + * @param array|null $injected + * @return bool + */ + public function create(User $user, ?array $injected = null): bool + { + return $user->can(CrudPermission::CREATE->format(ExternalProfile::class)); + } + + /** + * Determine whether the user can update the model. + * + * @param User $user + * @param array $injected + * @return bool + */ + public function update(User $user, array $injected): bool + { + /** @var ExternalProfile $profile */ + $profile = Arr::get($injected, 'id'); + + return !$profile->trashed() && $profile->user()->is($user) && $user->can(CrudPermission::UPDATE->format(ExternalProfile::class)); + } + + /** + * Determine whether the user can delete the model. + * + * @param User $user + * @param array $injected + * @return bool + */ + public function delete(User $user, array $injected): bool + { + /** @var ExternalProfile $profile */ + $profile = Arr::get($injected, 'id'); + + return !$profile->trashed() && $profile->user()->is($user) && $user->can(CrudPermission::DELETE->format(ExternalProfile::class)); + } + + /** + * Determine whether the user can restore the model. + * + * @param User $user + * @param array $injected + * @return bool + */ + public function restore(User $user, array $injected): bool + { + /** @var ExternalProfile $profile */ + $profile = Arr::get($injected, 'id'); + + return $profile->trashed() && $profile->user()->is($user) && $user->can(ExtendedCrudPermission::RESTORE->format(ExternalProfile::class)); + } +} diff --git a/app/GraphQL/Policies/List/Playlist/PlaylistTrackPolicy.php b/app/GraphQL/Policies/List/Playlist/PlaylistTrackPolicy.php new file mode 100644 index 000000000..45235c24a --- /dev/null +++ b/app/GraphQL/Policies/List/Playlist/PlaylistTrackPolicy.php @@ -0,0 +1,123 @@ +user()->is($user) || PlaylistVisibility::PRIVATE !== $playlist?->visibility) && $user->can(CrudPermission::VIEW->format(PlaylistTrack::class)) + : PlaylistVisibility::PRIVATE !== $playlist?->visibility; + } + + /** + * Determine whether the user can view the model. + * + * @param User|null $user + * @param array|null $injected + * @return bool + * + * @noinspection PhpUnusedParameterInspection + */ + public function view(?User $user, ?array $injected = null): bool + { + /** @var Playlist|null $playlist */ + $playlist = Arr::get($injected, 'playlist'); + + return $user !== null + ? ($playlist?->user()->is($user) || PlaylistVisibility::PRIVATE !== $playlist?->visibility) && $user->can(CrudPermission::VIEW->format(PlaylistTrack::class)) + : PlaylistVisibility::PRIVATE !== $playlist?->visibility; + } + + /** + * Determine whether the user can create models. + * + * @param User $user + * @param array|null $injected + * @return bool + */ + public function create(User $user, ?array $injected = null): bool + { + /** @var Playlist|null $playlist */ + $playlist = Arr::get($injected, 'playlist'); + + return $playlist?->user()->is($user); + } + + /** + * Determine whether the user can update the model. + * + * @param User $user + * @param array $injected + * @return bool + */ + public function update(User $user, array $injected): bool + { + /** @var Playlist|null $playlist */ + $playlist = Arr::get($injected, 'playlist'); + /** @var PlaylistTrack $track */ + $track = Arr::get($injected, PlaylistTrackMutator::ROUTE_SLUG); + + return !$track->trashed() && $playlist?->user()->is($user) && $user->can(CrudPermission::UPDATE->format(PlaylistTrack::class)); + } + + /** + * Determine whether the user can delete the model. + * + * @param User $user + * @param array $injected + * @return bool + */ + public function delete(User $user, array $injected): bool + { + /** @var Playlist|null $playlist */ + $playlist = Arr::get($injected, 'playlist'); + /** @var PlaylistTrack $track */ + $track = Arr::get($injected, PlaylistTrackMutator::ROUTE_SLUG); + + return !$track->trashed() && $playlist?->user()->is($user) && $user->can(CrudPermission::DELETE->format(PlaylistTrack::class)); + } + + /** + * Determine whether the user can restore the model. + * + * @param User $user + * @param array $injected + * @return bool + */ + public function restore(User $user, array $injected): bool + { + /** @var Playlist|null $playlist */ + $playlist = Arr::get($injected, 'playlist'); + /** @var PlaylistTrack $track */ + $track = Arr::get($injected, PlaylistTrackMutator::ROUTE_SLUG); + + return $track->trashed() && $playlist?->user()->is($user) && $user->can(ExtendedCrudPermission::RESTORE->format(PlaylistTrack::class)); + } +} diff --git a/app/GraphQL/Policies/List/PlaylistPolicy.php b/app/GraphQL/Policies/List/PlaylistPolicy.php new file mode 100644 index 000000000..6f84d390b --- /dev/null +++ b/app/GraphQL/Policies/List/PlaylistPolicy.php @@ -0,0 +1,106 @@ +can(CrudPermission::VIEW->format(Playlist::class)); + } + + /** + * Determine whether the user can view the model. + * + * @param User|null $user + * @param array|null $injected + * @return bool + */ + public function view(?User $user, ?array $injected = null): bool + { + /** @var Playlist $playlist */ + $playlist = Arr::get($injected, PlaylistMutator::ROUTE_SLUG); + + return $user !== null + ? ($playlist->user()->is($user) || PlaylistVisibility::PRIVATE !== $playlist->visibility) && $user->can(CrudPermission::VIEW->format(Playlist::class)) + : PlaylistVisibility::PRIVATE !== $playlist->visibility; + } + + /** + * Determine whether the user can create models. + * + * @param User $user + * @param array|null $injected + * @return bool + */ + public function create(User $user, ?array $injected = null): bool + { + return $user->can(CrudPermission::CREATE->format(Playlist::class)); + } + + /** + * Determine whether the user can update the model. + * + * @param User $user + * @param array $injected + * @return bool + */ + public function update(User $user, array $injected): bool + { + /** @var Playlist $playlist */ + $playlist = Arr::get($injected, PlaylistMutator::ROUTE_SLUG); + + return !$playlist->trashed() && $playlist->user()->is($user) && $user->can(CrudPermission::UPDATE->format(Playlist::class)); + } + + /** + * Determine whether the user can delete the model. + * + * @param User $user + * @param array $injected + * @return bool + */ + public function delete(User $user, array $injected): bool + { + /** @var Playlist $playlist */ + $playlist = Arr::get($injected, PlaylistMutator::ROUTE_SLUG); + + return !$playlist->trashed() && $playlist->user()->is($user) && $user->can(CrudPermission::DELETE->format(Playlist::class)); + } + + /** + * Determine whether the user can restore the model. + * + * @param User $user + * @param array $injected + * @return bool + */ + public function restore(User $user, array $injected): bool + { + /** @var Playlist $playlist */ + $playlist = Arr::get($injected, PlaylistMutator::ROUTE_SLUG); + + return $playlist->trashed() && $playlist->user()->is($user) && $user->can(ExtendedCrudPermission::RESTORE->format(Playlist::class)); + } +} diff --git a/app/GraphQL/Policies/Wiki/Anime/AnimeSynonymPolicy.php b/app/GraphQL/Policies/Wiki/Anime/AnimeSynonymPolicy.php new file mode 100644 index 000000000..1abca5958 --- /dev/null +++ b/app/GraphQL/Policies/Wiki/Anime/AnimeSynonymPolicy.php @@ -0,0 +1,14 @@ +distinct(Anime::ATTRIBUTE_YEAR) + ->orderBy(Anime::ATTRIBUTE_YEAR) + ->pluck(Anime::ATTRIBUTE_YEAR); + } + + /** + * Return a value for the field. + * + * @param null $root Always null, since this field has no parent. + * @param array $args The field arguments passed by the client. + * @param GraphQLContext $context Shared between all fields. + * @param ResolveInfo $resolveInfo Metadata for advanced query resolution. + * @return mixed The result of resolving the field, matching what was promised in the schema. + */ + public function year(null $root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): mixed + { + $year = Arr::get($args, 'year'); + + return Anime::query() + ->where(Anime::ATTRIBUTE_YEAR, $year) + ->orderBy(Anime::ATTRIBUTE_NAME) + ->get() + ->groupBy(fn (Anime $anime) => Str::lower($anime->season->localize())); + } +} diff --git a/app/GraphQL/Queries/Search.php b/app/GraphQL/Queries/Search.php new file mode 100644 index 000000000..37c8673b0 --- /dev/null +++ b/app/GraphQL/Queries/Search.php @@ -0,0 +1,76 @@ +getFieldSelection(); + + $term = Arr::get($args, 'search'); + $page = Arr::get($args, 'page'); + $perPage = Arr::get($args, 'perPage'); + + if (Arr::get($fields, 'anime')) { + $result['anime'] = Anime::search($term)->paginate($perPage, $page)->items(); + } + + if (Arr::get($fields, 'artists')) { + $result['artists'] = Artist::search($term)->paginate($perPage, $page)->items(); + } + + if (Arr::get($fields, 'animethemes')) { + $result['animethemes'] = AnimeTheme::search($term)->paginate($perPage, $page)->items(); + } + + if (Arr::get($fields, 'playlists')) { + $result['playlists'] = Playlist::search($term)->paginate($perPage, $page)->items(); + } + + if (Arr::get($fields, 'series')) { + $result['series'] = Series::search($term)->paginate($perPage, $page)->items(); + } + + if (Arr::get($fields, 'songs')) { + $result['songs'] = Song::search($term)->paginate($perPage, $page)->items(); + } + + if (Arr::get($fields, 'studios')) { + $result['studios'] = Studio::search($term)->paginate($perPage, $page)->items(); + } + + if (Arr::get($fields, 'videos')) { + $result['videos'] = Video::search($term)->paginate($perPage, $page)->items(); + } + + return $result; + } +} diff --git a/app/GraphQL/Resolvers/ExistsResolver.php b/app/GraphQL/Resolvers/ExistsResolver.php new file mode 100644 index 000000000..96983e45d --- /dev/null +++ b/app/GraphQL/Resolvers/ExistsResolver.php @@ -0,0 +1,33 @@ +fieldName); + + return $model->{$relation}->isNotEmpty(); + } +} diff --git a/app/GraphQL/Resolvers/ImplodeArrayResolver.php b/app/GraphQL/Resolvers/ImplodeArrayResolver.php new file mode 100644 index 000000000..24345ce35 --- /dev/null +++ b/app/GraphQL/Resolvers/ImplodeArrayResolver.php @@ -0,0 +1,29 @@ +getAttribute($resolveInfo->fieldName)); + } +} diff --git a/app/GraphQL/Resolvers/PivotResolver.php b/app/GraphQL/Resolvers/PivotResolver.php new file mode 100644 index 000000000..670c271e5 --- /dev/null +++ b/app/GraphQL/Resolvers/PivotResolver.php @@ -0,0 +1,36 @@ +getRelations()); + + return $pivot->{Str::snake($resolveInfo->fieldName)}; + } +} diff --git a/app/GraphQL/Resolvers/ViewsCountResolver.php b/app/GraphQL/Resolvers/ViewsCountResolver.php new file mode 100644 index 000000000..e79bdbdfd --- /dev/null +++ b/app/GraphQL/Resolvers/ViewsCountResolver.php @@ -0,0 +1,34 @@ +viewAggregate; + + return (int) $view?->value; + } +} diff --git a/app/GraphQL/Schemas/Admin/announcement.graphql b/app/GraphQL/Schemas/Admin/announcement.graphql new file mode 100644 index 000000000..d4b274ce0 --- /dev/null +++ b/app/GraphQL/Schemas/Admin/announcement.graphql @@ -0,0 +1,37 @@ +"Represents a site-wide message to be broadcasted on the homepage." +type Announcement { + "The primary key of the resource" + id: Int! @rename(attribute: "announcement_id") + "The announcement text" + content: String! + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +enum AnnouncementColumnsOrderable { + ID @enum(value: "announcement_id") + CONTENT + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of announcement resources." + announcements( + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "AnnouncementColumnsOrderable") + ): [Announcement!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Admin/dump.graphql b/app/GraphQL/Schemas/Admin/dump.graphql new file mode 100644 index 000000000..ca7072a31 --- /dev/null +++ b/app/GraphQL/Schemas/Admin/dump.graphql @@ -0,0 +1,44 @@ +""" +Represents a database dump of selected tables at a given point in time. + +For example, the animethemes-db-dump-wiki-1663559663946.sql dump represents the database dump of wiki tables performed at 2022-09-19. +""" +type Dump { + "The primary key of the resource" + id: Int! @rename(attribute: "dump_id") + "The path of the file in storage" + path: String! + "The URL to download the file from storage" + link: String! + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +enum DumpColumnsOrderable { + ID @enum(value: "dump_id") + PATH + LINK + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of dump resources given fields." + dumps( + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "DumpColumnsOrderable") + ): [Dump!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Admin/feature.graphql b/app/GraphQL/Schemas/Admin/feature.graphql new file mode 100644 index 000000000..e6acc3446 --- /dev/null +++ b/app/GraphQL/Schemas/Admin/feature.graphql @@ -0,0 +1,39 @@ +""" +Represents a feature flag that enable/disable site functionalities. + +For example, the 'allow_discord_notifications' feature enables/disables discord notifications for the configured bot. +""" +type Feature { + "The primary key of the resource" + id: Int! @rename(attribute: "feature_id") + "The title of the resource" + name: String! + "The value of the resource" + value: String! + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") +} + +enum FeatureColumnsOrderable { + ID @enum(value: "feature_id") + NAME + VALUE + CREATED_AT + UPDATED_AT +} + +extend type Query { + "Returns a listing of feature resources given fields." + features( + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "FeatureColumnsOrderable") + ): [Feature!]! + @canModel(ability: "viewAny", injectArgs: true) + @builder(method: "App\\GraphQL\\Builders\\Admin\\FeatureBuilder@index") + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Admin/featuredtheme.graphql b/app/GraphQL/Schemas/Admin/featuredtheme.graphql new file mode 100644 index 000000000..4f0ed93b1 --- /dev/null +++ b/app/GraphQL/Schemas/Admin/featuredtheme.graphql @@ -0,0 +1,53 @@ +"Represents a video to be featured on the homepage of the site for a specified amount of time." +type FeaturedTheme { + "The primary key of the resource" + id: Int! @rename(attribute: "featured_theme_id") + "The start date of the resource" + startAt: DateTimeTz! @rename(attribute: "start_at") + "The end date of the resource" + endAt: DateTimeTz! @rename(attribute: "end_at") + animethemeentry: AnimeThemeEntry @belongsTo + user: User @belongsTo + video: Video @belongsTo + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +enum FeaturedThemeColumnsOrderable { + ID @enum(value: "featured_theme_id") + START_AT + END_AT + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns the first featured theme where the current date is between start_at and end_at dates." + currentfeaturedtheme: FeaturedTheme @find + @canModel(ability: "viewAny", injectArgs: true) + @builder(method: "App\\GraphQL\\Builders\\Admin\\FeaturedThemeBuilder@current") + + "Returns a listing of featured theme resources given fields." + featuredthemes( + startAt_lesser: DateTimeTz @where(key: "start_at", operator: "<") + startAt_greater: DateTimeTz @where(key: "start_at", operator: ">") + endAt_lesser: DateTimeTz @where(key: "end_at", operator: "<") + endAt_greater: DateTimeTz @where(key: "end_at", operator: ">") + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "FeaturedThemeColumnsOrderable") + ): [FeaturedTheme!]! + @canModel(ability: "viewAny", injectArgs: true) + @builder(method: "App\\GraphQL\\Builders\\Admin\\FeaturedThemeBuilder@index") + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Auth/permission.graphql b/app/GraphQL/Schemas/Auth/permission.graphql new file mode 100644 index 000000000..e41bc0b1e --- /dev/null +++ b/app/GraphQL/Schemas/Auth/permission.graphql @@ -0,0 +1,15 @@ +""" +Represents an assignable label for users and roles that authorizes a particular action in AnimeThemes. +""" +type Permission { + "The primary key of the resource" + id: Int! + "The label of the resource" + name: String! + "The authentication guard of the resource" + guardName: String! @rename(attribute: "guard_name") + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Auth/role.graphql b/app/GraphQL/Schemas/Auth/role.graphql new file mode 100644 index 000000000..cc3e7dab1 --- /dev/null +++ b/app/GraphQL/Schemas/Auth/role.graphql @@ -0,0 +1,30 @@ +""" +Represents an assignable label for users that provides a configured group of permissions. +""" +type Role { + "The primary key of the resource" + id: Int! @rename(attribute: "role_id") + "The hex representation of the color used to distinguish the resource" + color: String! + "The label of the resource" + name: String! + "Is the role assigned on account verification?" + default: Boolean! + "The authentication guard of the resource" + guardName: String! @rename(attribute: "guard_name") + "The weight assigned to the resource, where higher values correspond to higher priority" + priority: Int! + permissions: [Permission] @belongsToMany(type: CONNECTION, edgeType: RolePermissionEdge) + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") +} + +type RolePermissionEdge { + node: Permission! + "The date that the resource was last modified" + createdAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was deleted" + updatedAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Auth/user.graphql b/app/GraphQL/Schemas/Auth/user.graphql new file mode 100644 index 000000000..52c481bfb --- /dev/null +++ b/app/GraphQL/Schemas/Auth/user.graphql @@ -0,0 +1,39 @@ +"Represents an AnimeThemes account." +type User { + "The username of the resource" + name: String! + playlists: [Playlist] @hasMany + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +"Represents the currently authenticated user." +type Me { + "The primary key of the resource" + id: Int + "The username of the resource" + name: String! + "The email of the user" + email: String! + "The date the user verified their email" + email_verified_at: DateTimeTz + "The date the user confirmed their two-factor authentication" + two_factor_confirmed_at: DateTimeTz + notifications: [Notification] @morphMany + playlists: [Playlist] @hasMany + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +extend type Query { + "Returns the data of the currently authenticated user." + me: Me @auth +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Document/page.graphql b/app/GraphQL/Schemas/Document/page.graphql new file mode 100644 index 000000000..ff03dd20e --- /dev/null +++ b/app/GraphQL/Schemas/Document/page.graphql @@ -0,0 +1,46 @@ +""" +Represents a static markdown page used for guides and other documentation. + +For example, the 'encoding/audio_normalization' page represents the documentation for audio normalization. +""" +type Page { + "The primary key of the resource" + id: Int! @rename(attribute: "page_id") + "The primary title of the page" + name: String! + "The URL slug & route key of the resource" + slug: String! + "The body content of the resource" + body: String + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +enum PageColumnsOrderable { + ID @enum(value: "page_id") + NAME + SLUG + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of page resources given fields." + pages( + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "PageColumnsOrderable") + ): [Page!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/List/External/externalentry.graphql b/app/GraphQL/Schemas/List/External/externalentry.graphql new file mode 100644 index 000000000..bdbd126b9 --- /dev/null +++ b/app/GraphQL/Schemas/List/External/externalentry.graphql @@ -0,0 +1,59 @@ +""" +Represents an anime entry on the external profile. + +For example, Hibike Euphonium! is marked as completed on the profile AnimeThemes. +""" +type ExternalEntry { + "The primary key of the resource" + id: Int! @rename(attribute: "entry_id") + "The score of the entry on the external site" + score: Float + "The favorite state of the entry on the external site" + isFavorite: Boolean! @rename(attribute: "is_favorite") + "The watch status of the entry on the external site" + watchStatus: ExternalEntryWatchStatus! @rename(attribute: "watch_status") @localizedEnum + anime: Anime @belongsTo + externalprofile: ExternalProfile @belongsTo + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +enum ExternalEntryColumnsOrderable { + ID @enum(value: "entry_id") + SCORE + IS_FAVORITE + WATCH_STATUS + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns an external entry resource." + externalentry( + profile: Int! @bind(class: "App\\Models\\List\\ExternalProfile") + id: Int! @bind(class: "App\\Models\\List\\External\\ExternalEntry") + ): ExternalEntry @first + @canModel(ability: "view", injectArgs: true) + + "Returns a listing of entries for the profile." + externalentries( + score_lesser: Int @where(key: "score", operator: "<") + score_greater: Int @where(key: "score", operator: ">") + watchStatus: [ExternalEntryWatchStatus] @in(key: "watch_status") + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "ExternalEntryColumnsOrderable") + ): [ExternalEntry!]! + @canModel(ability: "viewAny", injectArgs: true) + @builder(method: "App\\GraphQL\\Builders\\List\\External\\ExternalEntryBuilder@index") + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/List/Playlist/playlisttrack.graphql b/app/GraphQL/Schemas/List/Playlist/playlisttrack.graphql new file mode 100644 index 000000000..b5b524110 --- /dev/null +++ b/app/GraphQL/Schemas/List/Playlist/playlisttrack.graphql @@ -0,0 +1,100 @@ +""" +Represents an entry in a playlist. + +For example, a "/r/anime's Best OPs and EDs of 2022" playlist may contain a track for the ParipiKoumei-OP1.webm video. +""" +type PlaylistTrack { + "The primary key of the resource" + id: String! @rename(attribute: "hashid") + animethemeentry: AnimeThemeEntry @belongsTo + next: PlaylistTrack @belongsTo + playlist: Playlist @belongsTo + previous: PlaylistTrack @belongsTo + video: Video @belongsTo + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +enum PlaylistTrackColumnsOrderable { + ID @enum(value: "track_id") + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a playlist track resource." + playlisttrack( + playlist: String! @bind(class: "App\\Models\\List\\Playlist", column: "hashid") + id: String! @bind(class: "App\\Models\\List\\Playlist\\PlaylistTrack", column: "hashid") + ): PlaylistTrack @first + @canModel(ability: "view", injectArgs: true) + + "Returns a listing of tracks for the playlist." + playlisttracks( + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "PlaylistTrackColumnsOrderable") + ): [PlaylistTrack!]! + @canModel(ability: "viewAny", injectArgs: true) + @builder(method: "App\\GraphQL\\Builders\\List\\Playlist\\PlaylistTrackBuilder@index") + @paginate +} + +extend type Mutation @guard { + createPlaylistTrack( + entry_id: Int! + video_id: Int! + next: String + previous: String + playlist: String! @bind(class: "App\\Models\\List\\Playlist", column: "hashid") + ): PlaylistTrack! + @canModel(ability: "create", injectArgs: true) + @validator(class: "App\\GraphQL\\Validators\\Mutation\\List\\Playlist\\CreatePlaylistTrackValidator") + @field(resolver: "App\\GraphQL\\Mutations\\List\\Playlist\\PlaylistTrackMutator@store") + + + updatePlaylistTrack( + id: String! @bind(class: "App\\Models\\List\\Playlist\\PlaylistTrack", column: "hashid") + entry_id: Int + video_id: Int + next: String + previous: String + playlist: String! @bind(class: "App\\Models\\List\\Playlist", column: "hashid") + ): PlaylistTrack! + @canModel(ability: "update", injectArgs: true) + @validator(class: "App\\GraphQL\\Validators\\Mutation\\List\\Playlist\\UpdatePlaylistTrackValidator") + @field(resolver: "App\\GraphQL\\Mutations\\List\\Playlist\\PlaylistTrackMutator@update") + + + deletePlaylistTrack( + id: String! @bind(class: "App\\Models\\List\\Playlist\\PlaylistTrack", column: "hashid") + playlist: String! @bind(class: "App\\Models\\List\\Playlist", column: "hashid") + ): PlaylistTrack! + @canModel(ability: "delete", injectArgs: true) + @field(resolver: "App\\GraphQL\\Mutations\\List\\Playlist\\PlaylistTrackMutator@destroy") + + + restorePlaylistTrack( + id: String! @bind(class: "App\\Models\\List\\Playlist\\PlaylistTrack", column: "hashid") + playlist: String! @bind(class: "App\\Models\\List\\Playlist", column: "hashid") + ): PlaylistTrack! + @canModel(ability: "restore", injectArgs: true) + @field(resolver: "App\\GraphQL\\Mutations\\List\\Playlist\\PlaylistTrackMutator@restore") + + + forceDeletePlaylistTrack( + id: String! @bind(class: "App\\Models\\List\\Playlist\\PlaylistTrack", column: "hashid") + playlist: String! @bind(class: "App\\Models\\List\\Playlist", column: "hashid") + ): MessageResponse! + @canModel(ability: "forceDelete", injectArgs: true, model: "App\\Models\\List\\Playlist\\PlaylisTrack") + @field(resolver: "App\\GraphQL\\Mutations\\List\\Playlist\\PlaylistTrackMutator@forceDelete") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/List/externalprofile.graphql b/app/GraphQL/Schemas/List/externalprofile.graphql new file mode 100644 index 000000000..52c1ebfd0 --- /dev/null +++ b/app/GraphQL/Schemas/List/externalprofile.graphql @@ -0,0 +1,62 @@ +"Represents a user profile on the external site like MAL." +type ExternalProfile { + "The primary key of the resource" + id: Int! @rename(attribute: "profile_id") + "The title of the profile" + name: String! + "The site the profile belongs to" + site: ExternalProfileSite! @localizedEnum + "The state of who can see the profile" + visibility: ExternalProfileVisibility! @localizedEnum + externalentries: [ExternalEntry] @hasMany + user: User @belongsTo + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +enum ExternalProfileColumnsOrderable { + ID @enum(value: "profile_id") + NAME + SITE + VISIBILITY + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query @middleware(class: "App\\Http\\Middleware\\Api\\EnabledOnlyOnLocalhost") { + "Returns an external profile resource." + externalprofile( + id: String! @bind(class: "App\\Models\\List\\ExternalProfile") + ): ExternalProfile @first + @canModel(ability: "view", injectArgs: true) + + "Returns a listing of external profile resources." + externalprofiles( + name: String @eq + site: [ExternalProfileSite] @in + visibility: [ExternalProfileVisibility] @in + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "ExternalProfileColumnsOrderable") + ): [ExternalProfile!]! + @canModel(ability: "viewAny", injectArgs: true) + @builder(method: "App\\GraphQL\\Builders\\List\\ExternalProfileBuilder@index") + @paginate +} + +extend type Mutation @guard @middleware(class: "App\\Http\\Middleware\\Api\\EnabledOnlyOnLocalhost") @feature(name: "App\\Features\\AllowExternalProfileManagement") { + syncExternalProfile( + id: Int! @bind(class: "App\\Models\\List\\ExternalProfile", column: "profile_id") + ): MessageResponse! + @canModel(ability: "update", injectArgs: true, model: "App\\Models\\List\\ExternalProfile") + @field(resolver: "App\\GraphQL\\Mutations\\List\\ExternalProfileMutator@sync") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/List/playlist.graphql b/app/GraphQL/Schemas/List/playlist.graphql new file mode 100644 index 000000000..150a992ab --- /dev/null +++ b/app/GraphQL/Schemas/List/playlist.graphql @@ -0,0 +1,117 @@ +""" +Represents a list of ordered tracks intended for continuous playback. + +For example, a "/r/anime's Best OPs and EDs of 2022" playlist may contain a collection of tracks allowing the continuous playback of Best OP and ED nominations for the /r/anime Awards. +""" +type Playlist { + "The primary key of the resource" + id: String! @rename(attribute: "hashid") + "The title of the playlist" + name: String! + "The description of the playlist" + description: String + "The number of tracks belonging to the resource" + tracksCount: Int! @count(relation: "tracks") + "The existence of tracks belonging to the resource" + tracksExists: Boolean! @with(relation: "tracks") @field(resolver: "App\\GraphQL\\Resolvers\\ExistsResolver@resolve") + "The state of who can see the playlist" + visibility: PlaylistVisibility! @localizedEnum + "The number of views recorded for the resource" + viewsCount: Int! @with(relation: "viewAggregate") @field(resolver: "App\\GraphQL\\Resolvers\\ViewsCountResolver@resolve") + first: PlaylistTrack @belongsTo + images: [Image] @belongsToMany(type: CONNECTION, edgeType: PlaylistImageEdge) + last: PlaylistTrack @belongsTo + tracks: [PlaylistTrack] @hasMany + user: User @belongsTo + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +type PlaylistImageEdge { + node: Image! + "The date that the resource was last modified" + createdAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was deleted" + updatedAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} + +enum PlaylistColumnsOrderable { + ID @enum(value: "playlist_id") + NAME + DESCRIPTION + VISIBILITY + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a playlist resource." + playlist( + id: String! @bind(class: "App\\Models\\List\\Playlist", column: "hashid") + ): Playlist @first + @canModel(ability: "view", injectArgs: true) + + "Returns a listing of playlist resources." + playlists( + name: String @eq + visibility: [PlaylistVisibility] @in + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "PlaylistColumnsOrderable") + ): [Playlist!]! + @canModel(ability: "viewAny", injectArgs: true) + @builder(method: "App\\GraphQL\\Builders\\List\\PlaylistBuilder@index") + @paginate +} + +extend type Mutation @guard { + createPlaylist( + name: String! + description: String + visibility: PlaylistVisibility! + ): Playlist! + @canModel(ability: "create") + @validator(class: "App\\GraphQL\\Validators\\Mutation\\List\\CreatePlaylistValidator") + @field(resolver: "App\\GraphQL\\Mutations\\List\\PlaylistMutator@store") + + + updatePlaylist( + id: String! @bind(class: "App\\Models\\List\\Playlist", column: "hashid") + name: String + description: String + visibility: PlaylistVisibility + ): Playlist! + @canModel(ability: "update", injectArgs: true) + @validator(class: "App\\GraphQL\\Validators\\Mutation\\List\\UpdatePlaylistValidator") + @field(resolver: "App\\GraphQL\\Mutations\\List\\PlaylistMutator@update") + + + deletePlaylist( + id: String! @bind(class: "App\\Models\\List\\Playlist", column: "hashid") + ): Playlist! + @canModel(ability: "delete", injectArgs: true) + @field(resolver: "App\\GraphQL\\Mutations\\List\\PlaylistMutator@destroy") + + + restorePlaylist( + id: String! @bind(class: "App\\Models\\List\\Playlist", column: "hashid") + ): Playlist! + @canModel(ability: "restore", injectArgs: true) + @field(resolver: "App\\GraphQL\\Mutations\\List\\PlaylistMutator@restore") + + + forceDeletePlaylist( + id: String! @bind(class: "App\\Models\\List\\Playlist", column: "hashid") + ): MessageResponse! + @canModel(ability: "forceDelete", injectArgs: true, model: "App\\Models\\List\\Playlist") + @field(resolver: "App\\GraphQL\\Mutations\\List\\PlaylistMutator@forceDelete") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Pivot/List/playlistimage.graphql b/app/GraphQL/Schemas/Pivot/List/playlistimage.graphql new file mode 100644 index 000000000..32712431a --- /dev/null +++ b/app/GraphQL/Schemas/Pivot/List/playlistimage.graphql @@ -0,0 +1,9 @@ +"Represents the association between a playlist and an image." +type PlaylistImage { + playlist: Playlist! + image: Image! + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Pivot/Wiki/animeimage.graphql b/app/GraphQL/Schemas/Pivot/Wiki/animeimage.graphql new file mode 100644 index 000000000..d485b70c8 --- /dev/null +++ b/app/GraphQL/Schemas/Pivot/Wiki/animeimage.graphql @@ -0,0 +1,9 @@ +"Represents the association between an anime and an image." +type AnimeImage { + anime: Anime! + image: Image! + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Pivot/Wiki/animeresource.graphql b/app/GraphQL/Schemas/Pivot/Wiki/animeresource.graphql new file mode 100644 index 000000000..8c1e555d1 --- /dev/null +++ b/app/GraphQL/Schemas/Pivot/Wiki/animeresource.graphql @@ -0,0 +1,11 @@ +"Represents the association between an anime and an external resource." +type AnimeResource { + anime: Anime! + resource: ExternalResource! + "Used to distinguish resources that map to the same anime" + as: String + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Pivot/Wiki/animeseries.graphql b/app/GraphQL/Schemas/Pivot/Wiki/animeseries.graphql new file mode 100644 index 000000000..a00949ee9 --- /dev/null +++ b/app/GraphQL/Schemas/Pivot/Wiki/animeseries.graphql @@ -0,0 +1,9 @@ +"Represents the association between an anime and a series." +type AnimeSeries { + anime: Anime! + series: Series! + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Pivot/Wiki/animestudio.graphql b/app/GraphQL/Schemas/Pivot/Wiki/animestudio.graphql new file mode 100644 index 000000000..b5a2cd0d8 --- /dev/null +++ b/app/GraphQL/Schemas/Pivot/Wiki/animestudio.graphql @@ -0,0 +1,9 @@ +"Represents the association between an anime and a studio." +type AnimeStudio { + anime: Anime! + studio: Studio! + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Pivot/Wiki/animethemeentryvideo.graphql b/app/GraphQL/Schemas/Pivot/Wiki/animethemeentryvideo.graphql new file mode 100644 index 000000000..32d0b135c --- /dev/null +++ b/app/GraphQL/Schemas/Pivot/Wiki/animethemeentryvideo.graphql @@ -0,0 +1,9 @@ +"Represents the association between an anime theme entry and a video." +type AnimeThemeEntryVideo { + animethemeentry: AnimeThemeEntry! + video: Video! + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Pivot/Wiki/artistimage.graphql b/app/GraphQL/Schemas/Pivot/Wiki/artistimage.graphql new file mode 100644 index 000000000..bdd5c6e73 --- /dev/null +++ b/app/GraphQL/Schemas/Pivot/Wiki/artistimage.graphql @@ -0,0 +1,11 @@ +"Represents the association between an artist and an image." +type ArtistImage { + artist: Artist! + image: Image! + "Used to sort the artist images" + depth: Int + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Pivot/Wiki/artistmember.graphql b/app/GraphQL/Schemas/Pivot/Wiki/artistmember.graphql new file mode 100644 index 000000000..bdf5d7179 --- /dev/null +++ b/app/GraphQL/Schemas/Pivot/Wiki/artistmember.graphql @@ -0,0 +1,15 @@ +"Represents the association of an artist and a group/unit." +type ArtistMember { + artist: Artist! + member: Artist! + "Used to distinguish member by alias" + alias: String + "Used to distinguish member by character" + as: String + "Used to extra annotation, like member role" + notes: String + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Pivot/Wiki/artistresource.graphql b/app/GraphQL/Schemas/Pivot/Wiki/artistresource.graphql new file mode 100644 index 000000000..e7e9e4cdc --- /dev/null +++ b/app/GraphQL/Schemas/Pivot/Wiki/artistresource.graphql @@ -0,0 +1,11 @@ +"Represents the association between an artist and an external resource." +type ArtistResource { + artist: Artist! + resource: ExternalResource! + "Used to distinguish resources that map to the same artist" + as: String + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Pivot/Wiki/songresource.graphql b/app/GraphQL/Schemas/Pivot/Wiki/songresource.graphql new file mode 100644 index 000000000..daa0793ed --- /dev/null +++ b/app/GraphQL/Schemas/Pivot/Wiki/songresource.graphql @@ -0,0 +1,11 @@ +"Represents the association between an song and an external resource." +type SongResource { + song: Song! + resource: ExternalResource! + "Used to distinguish resources that map to the same song" + as: String + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Pivot/Wiki/studioimage.graphql b/app/GraphQL/Schemas/Pivot/Wiki/studioimage.graphql new file mode 100644 index 000000000..edac71860 --- /dev/null +++ b/app/GraphQL/Schemas/Pivot/Wiki/studioimage.graphql @@ -0,0 +1,9 @@ +"Represents the association between a studio and an image." +type StudioImage { + studio: Studio! + image: Image! + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Pivot/Wiki/studioresource.graphql b/app/GraphQL/Schemas/Pivot/Wiki/studioresource.graphql new file mode 100644 index 000000000..9b1f2afd6 --- /dev/null +++ b/app/GraphQL/Schemas/Pivot/Wiki/studioresource.graphql @@ -0,0 +1,11 @@ +"Represents the association between a studio and an external resource." +type StudioResource { + studio: Studio! + resource: ExternalResource! + "Used to distinguish resources that map to the same studio" + as: String + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/User/notification.graphql b/app/GraphQL/Schemas/User/notification.graphql new file mode 100644 index 000000000..9bb6872e5 --- /dev/null +++ b/app/GraphQL/Schemas/User/notification.graphql @@ -0,0 +1,19 @@ +"Represents a notification that is sent to the user." +type Notification { + "The date that the user read the notification" + readAt: DateTimeTz @rename(attribute: "read_at") + "The JSON data of the notification" + data: NotificationData! + "The user the notification belongs to" + user: User! @morphTo(relation: "notifiable") +} + +"Represents the JSON data of the notification" +type NotificationData { + "The title of the notification" + title: String! + "The content of the notification" + body: String! + "The image URL to display with the notification" + image: String +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Wiki/Anime/Theme/animethemeentry.graphql b/app/GraphQL/Schemas/Wiki/Anime/Theme/animethemeentry.graphql new file mode 100644 index 000000000..17dd3055b --- /dev/null +++ b/app/GraphQL/Schemas/Wiki/Anime/Theme/animethemeentry.graphql @@ -0,0 +1,68 @@ +""" +Represents a version of an anime theme. + +For example, the ED theme of the Bakemonogatari anime has three anime theme entries to represent three versions. +""" +type AnimeThemeEntry { + "The primary key of the resource" + id: Int! @rename(attribute: "entry_id") + "The episodes that the theme is used for" + episodes: String + "Any additional information for this sequence" + notes: String + "Is not safe for work content included?" + nsfw: Boolean! + "Is content included that may spoil the viewer?" + spoiler: Boolean! + "The version number of the theme" + version: Int + animetheme: AnimeTheme @belongsTo + videos: [Video] @belongsToMany(type: CONNECTION) + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +"Default edge to use in simple belongs to many relationships" +type AnimeThemeEntryEdge { + node: AnimeThemeEntry! + "The date that the resource was last modified" + createdAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was deleted" + updatedAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} + +enum AnimeThemeEntryColumnsOrderable { + ID @enum(value: "entry_id") + EPISODES + NOTES + NSFW + SPOILER + VERSION + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of anime theme entry resources given fields." + animethemeentries( + isNsfw: Boolean @eq + isSpoiler: Boolean @eq + version_lesser: Int @where(key: "version", operator: "<") + version_greater: Int @where(key: "version", operator: ">") + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "AnimeThemeEntryColumnsOrderable") + ): [AnimeThemeEntry!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Wiki/Anime/animesynonym.graphql b/app/GraphQL/Schemas/Wiki/Anime/animesynonym.graphql new file mode 100644 index 000000000..89a55f871 --- /dev/null +++ b/app/GraphQL/Schemas/Wiki/Anime/animesynonym.graphql @@ -0,0 +1,46 @@ +""" +Represents an alternate title or common abbreviation for an anime. + +For example, the anime Bakemonogatari has the anime synonym "Monstory". +""" +type AnimeSynonym { + "The primary key of the resource" + id: Int! @rename(attribute: "synonym_id") + "The alternate title or common abbreviations" + name: String! + "The type of the synonym" + type: AnimeSynonymType! @localizedEnum + anime: Anime @belongsTo + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +enum AnimeSynonymColumnsOrderable { + ID @enum(value: "synonym_id") + NAME + TYPE + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of anime synonym resources given fields." + animesynonyms( + type: [AnimeSynonymType] @in + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "AnimeSynonymColumnsOrderable") + ): [AnimeSynonym!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Wiki/Anime/animetheme.graphql b/app/GraphQL/Schemas/Wiki/Anime/animetheme.graphql new file mode 100644 index 000000000..832ff51ca --- /dev/null +++ b/app/GraphQL/Schemas/Wiki/Anime/animetheme.graphql @@ -0,0 +1,51 @@ +""" +Represents an OP or ED sequence for an anime. + +For example, the anime Bakemonogatari has five OP anime themes and one ED anime theme. +""" +type AnimeTheme { + "The primary key of the resource" + id: Int! @rename(attribute: "theme_id") + "The type of the sequence" + type: ThemeType! @localizedEnum + "The numeric ordering of the theme" + sequence: Int + anime: Anime @belongsTo + animethemeentries: [AnimeThemeEntry] @hasMany + group: ThemeGroup @belongsTo + song: Song @belongsTo + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +enum AnimeThemeColumnsOrderable { + ID @enum(value: "theme_id") + TYPE + SEQUENCE + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of anime theme resources given fields." + animethemes( + type: [ThemeType] @in + sequence_lesser: Int @where(key: "sequence", operator: "<") + sequence_greater: Int @where(key: "sequence", operator: ">") + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "AnimeThemeColumnsOrderable") + ): [AnimeTheme!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Wiki/Song/membership.graphql b/app/GraphQL/Schemas/Wiki/Song/membership.graphql new file mode 100644 index 000000000..6aa61bd53 --- /dev/null +++ b/app/GraphQL/Schemas/Wiki/Song/membership.graphql @@ -0,0 +1,49 @@ +""" +Represents the link between an artist and a group related to the song credits. + +For example, Sayuri Date is a member of Liella and has performed using the membership. +""" +type Membership { + "The primary key of the resource" + id: Int! @rename(attribute: "membership_id") + "The alias the artist is using for this membership" + alias: String + "The character the artist is performing as" + as: String + group: Artist! @belongsTo(relation: "artist") + member: Artist! @belongsTo + performances: [Performance] @morphMany + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +enum MembershipColumnsOrderable { + ID @enum(value: "membership_id") + ALIAS + AS + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of membership resources given fields." + memberships( + alias: String @eq + as: String @eq + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "MembershipColumnsOrderable") + ): [Membership!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Wiki/Song/performance.graphql b/app/GraphQL/Schemas/Wiki/Song/performance.graphql new file mode 100644 index 000000000..a93260ad1 --- /dev/null +++ b/app/GraphQL/Schemas/Wiki/Song/performance.graphql @@ -0,0 +1,54 @@ +""" +Represents the link between a song and an artist or membership. + +For example, Liella has performed using memberships, with its members credited. +""" +type Performance { + "The primary key of the resource" + id: Int! @rename(attribute: "performance_id") + "The song the artist is performing" + song: Song @belongsTo + "The artist or membership performing" + artist: PerformanceArtistUnion! @morphTo + "The alias the artist is using for this performance" + alias: String + "The character the artist is performing as" + as: String + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +"Represents the resource type performing" +union PerformanceArtistUnion = Artist | Membership + +enum PerformanceColumnsOrderable { + ID @enum(value: "performance_id") + ALIAS + AS + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of performance resources given fields." + performances( + alias: String @eq + as: String @eq + hasSong: [WhereHasConditions!] @whereHasConditions(relation: "song", columns: ["title"]) + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "PerformanceColumnsOrderable") + ): [Performance!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Wiki/Video/videoscript.graphql b/app/GraphQL/Schemas/Wiki/Video/videoscript.graphql new file mode 100644 index 000000000..ad228f6ca --- /dev/null +++ b/app/GraphQL/Schemas/Wiki/Video/videoscript.graphql @@ -0,0 +1,45 @@ +""" +Represents an encoding script used to produce a video. + +For example, the 2009/Summer/Bakemonogatari-OP1.txt video script represents the encoding script of the Bakemonogatari-OP1.webm video. +""" +type VideoScript { + "The primary key of the resource" + id: Int! @rename(attribute: "script_id") + "The path of the file in storage" + path: String! + "The URL to download the file from storage" + link: String! + video: Video @hasOne + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +enum VideoScriptColumnsOrderable { + ID @enum(value: "script_id") + PATH + LINK + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of script resources given fields." + scripts( + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "VideoScriptColumnsOrderable") + ): [VideoScript!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Wiki/anime.graphql b/app/GraphQL/Schemas/Wiki/anime.graphql new file mode 100644 index 000000000..fbb43c5d7 --- /dev/null +++ b/app/GraphQL/Schemas/Wiki/anime.graphql @@ -0,0 +1,104 @@ +""" +Represents a production with at least one opening or ending sequence. + +For example, Bakemonogatari is an anime production with five opening sequences and one ending sequence." +""" +type Anime { + "The primary key of the resource" + id: Int! @rename(attribute: "anime_id") + "The media format of the anime" + mediaFormat: AnimeMediaFormat @rename(attribute: "media_format") @localizedEnum + "The primary title of the anime" + name: String! + "The premiere season of the anime" + season: AnimeSeason @localizedEnum + "The URL slug & route key of the resource" + slug: String! + "The brief summary of the anime" + synopsis: String + "The premiere year of the anime" + year: Int + animesynonyms: [AnimeSynonym] @hasMany + animethemes: [AnimeTheme] @hasMany + images: [Image]! @belongsToMany(type: CONNECTION) + resources: [ExternalResource]! @belongsToMany(type: CONNECTION) + series: [Series]! @belongsToMany(type: CONNECTION) + studios: [Studio]! @belongsToMany(type: CONNECTION) + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +"Default edge to use in simple belongs to many relationships" +type AnimeEdge { + node: Anime! + "The date that the resource was last modified" + createdAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was deleted" + updatedAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} + +type AnimeYear { + winter: [Anime] + spring: [Anime] + summer: [Anime] + fall: [Anime] +} + +enum AnimeColumnsOrderable { + ID @enum(value: "anime_id") + NAME + MEDIA_FORMAT + SEASON + SLUG + YEAR + CREATED_AT + UPDATED_AT + DELETED_AT +} + +enum HasResources { + SITE + EXTERNAL_ID +} + +extend type Query { + "Returns a listing of anime resources given fields." + animes( + search: String @search + name: String @where(operator: "like") + mediaFormat: [AnimeMediaFormat] @in(key: "media_format") + season: [AnimeSeason] @in + slug: String @eq + year_in: [Int] @in(key: "year") + year_notin: [Int] @notIn(key: "year") + year_lesser: Int @where(key: "year", operator: "<") + year_greater: Int @where(key: "year", operator: ">") + where: _ @whereConditions(columns: ["name"]) + whereHas: [WhereHasConditions!] @whereHasConditions(columns: ["*"]) + hasResources: [WhereHasConditions!] @whereHasConditions(columnsEnum: "HasResources") + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "AnimeColumnsOrderable") + ): [Anime!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate + + "Returns a listing of anime resources for a given year grouped by season and ordered by name." + animeyear(year: Int!): AnimeYear + @canModel(ability: "viewAny", model: "App\\Models\\Wiki\\Anime", injectArgs: true) + @field(resolver: "App\\GraphQL\\Queries\\AnimeYear@year") + + "Returns a list of unique years from all anime resources." + animeyears: [Int!]! + @canModel(ability: "viewAny", model: "App\\Models\\Wiki\\Anime", injectArgs: true) + @field(resolver: "App\\GraphQL\\Queries\\AnimeYear@years") +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Wiki/artist.graphql b/app/GraphQL/Schemas/Wiki/artist.graphql new file mode 100644 index 000000000..caf65101a --- /dev/null +++ b/app/GraphQL/Schemas/Wiki/artist.graphql @@ -0,0 +1,83 @@ +""" +Represents a musical performer of anime sequences. + +For example, Chiwa Saitou is the musical performer of the Bakemonogatari OP1 theme, among many others. +""" +type Artist { + "The primary key of the resource" + id: Int! @rename(attribute: "artist_id") + "The primary title of the artist" + name: String! + "The URL slug & route key of the resource" + slug: String! + "The brief information of the resource" + information: String + groups: [Artist] @belongsToMany(type: CONNECTION, edgeType: ArtistMemberEdge) + images: [Image] @belongsToMany(type: CONNECTION, edgeType: ArtistImageEdge) + members: [Artist] @belongsToMany(type: CONNECTION, edgeType: ArtistMemberEdge) + memberships: [Membership] @hasMany + performances: [Performance] @morphMany + resources: [ExternalResource]! @belongsToMany(type: CONNECTION) + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +"Default edge to use in simple belongs to many relationships" +type ArtistEdge { + node: Artist! + "The date that the resource was last modified" + createdAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was deleted" + updatedAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} + +"Artist Member has extra pivots" +type ArtistMemberEdge { + node: Artist! + "Used to distinguish member by alias" + alias: String @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "Used to distinguish member by character" + as: String @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "Used to extra annotation, like member role" + notes: String @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} + +"Artist Image has an extra pivot that others image relationships don't" +type ArtistImageEdge { + node: Image! + "Used to sort the artist images" + depth: Int @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was last modified" + createdAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was deleted" + updatedAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} + +enum ArtistColumnsOrderable { + ID @enum(value: "artist_id") + NAME + SLUG + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of artist resources given fields." + artists( + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "ArtistColumnsOrderable") + ): [Artist!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Wiki/audio.graphql b/app/GraphQL/Schemas/Wiki/audio.graphql new file mode 100644 index 000000000..58d5f5c16 --- /dev/null +++ b/app/GraphQL/Schemas/Wiki/audio.graphql @@ -0,0 +1,58 @@ +""" +Represents the audio track of a video. + +For example, the audio Bakemonogatari-OP1.ogg represents the audio track of the Bakemonogatari-OP1.webm video. +""" +type Audio { + "The primary key of the resource" + id: Int! @rename(attribute: "audio_id") + "The basename of the file in storage" + basename: String! + "The filename of the file in storage" + filename: String! + "The media type of the file in storage" + mimetype: String! + "The path of the file in storage" + path: String! + "The size of the file in storage in Bytes" + size: Int! + "The URL to stream the file from storage" + link: String! + "The number of views recorded for the resource" + viewsCount: Int! @with(relation: "viewAggregate") @field(resolver: "App\\GraphQL\\Resolvers\\ViewsCountResolver@resolve") + videos: [Video] @hasMany + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +enum AudioColumnsOrderable { + ID @enum(value: "audio_id") + FILENAME + BASENAME + SIZE + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of audio resources given fields." + audios( + size_lesser: Int @where(key: "size", operator: "<") + size_greater: Int @where(key: "size", operator: ">") + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "AudioColumnsOrderable") + ): [Audio!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Wiki/externalresource.graphql b/app/GraphQL/Schemas/Wiki/externalresource.graphql new file mode 100644 index 000000000..03d56e9ab --- /dev/null +++ b/app/GraphQL/Schemas/Wiki/externalresource.graphql @@ -0,0 +1,108 @@ +""" +Represents a site with supplementary information for another resource such as an anime or artist. + +For example, the Bakemonogatari anime has MyAnimeList, AniList and AniDB resources. +""" +type ExternalResource { + "The primary key of the resource" + id: Int! @rename(attribute: "resource_id") + "The primary key of the resource in the external site" + externalId: Int @rename(attribute: "external_id") + "The URL of the external site" + link: String! + "The external site that the resource belongs to" + site: ResourceSite! @localizedEnum + anime: [Anime] @belongsToMany(type: CONNECTION, edgeType: ExternalResourceAnimeEdge) + artists: [Artist] @belongsToMany(type: CONNECTION, edgeType: ExternalResourceArtistEdge) + songs: [Song] @belongsToMany(type: CONNECTION, edgeType: ExternalResourceSongEdge) + studios: [Studio] @belongsToMany(type: CONNECTION, edgeType: ExternalResourceStudioEdge) + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +"Default edge to use in simple belongs to many relationships" +type ExternalResourceEdge { + node: ExternalResource! + "Used to distinguish resources that map to the same resource" + as: String @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was last modified" + createdAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was deleted" + updatedAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} + +"External Resource has an extra pivot field" +type ExternalResourceAnimeEdge { + node: Anime! + "Used to distinguish resources that map to the same anime" + as: String @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was last modified" + createdAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was deleted" + updatedAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} + +"External Resource has an extra pivot field" +type ExternalResourceArtistEdge { + node: Artist! + "Used to distinguish resources that map to the same artist" + as: String @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was last modified" + createdAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was deleted" + updatedAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} + +"External Resource has an extra pivot field" +type ExternalResourceSongEdge { + node: Song! + "Used to distinguish resources that map to the same song" + as: String @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was last modified" + createdAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was deleted" + updatedAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} + +"External Resource has an extra pivot field" +type ExternalResourceStudioEdge { + node: Studio! + "Used to distinguish resources that map to the same studio" + as: String @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was last modified" + createdAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was deleted" + updatedAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} + +enum ExternalResourceColumnsOrderable { + ID @enum(value: "resource_id") + SITE + EXTERNAL_ID + LINK + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of resources given fields." + externalresources( + externalId: Int @eq + site: [ResourceSite] @in + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "ExternalResourceColumnsOrderable") + ): [ExternalResource!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Wiki/group.graphql b/app/GraphQL/Schemas/Wiki/group.graphql new file mode 100644 index 000000000..c17ed3e60 --- /dev/null +++ b/app/GraphQL/Schemas/Wiki/group.graphql @@ -0,0 +1,46 @@ +""" +Represents the group that accompanies a Theme. + +For example, English Version is the group for english dubbed Theme. +""" +type ThemeGroup @model(class: "App\\Models\\Wiki\\Group") { + "The primary key of the resource" + id: Int! @rename(attribute: "group_id") + "The name of the group" + name: String! + "The slug of the group" + slug: String! + animethemes: [AnimeTheme] @hasMany + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +enum ThemeGroupColumnsOrderable { + ID @enum(value: "group_id") + NAME + SLUG + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of theme groups resources given fields." + themegroups( + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "ThemeGroupColumnsOrderable") + ): [ThemeGroup!]! + + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Wiki/image.graphql b/app/GraphQL/Schemas/Wiki/image.graphql new file mode 100644 index 000000000..33f7a9b8d --- /dev/null +++ b/app/GraphQL/Schemas/Wiki/image.graphql @@ -0,0 +1,70 @@ +""" +Represents a visual component for another resource such as an anime or artist. + +For example, the Bakemonogatari anime has two images to represent small and large cover images. +""" +type Image { + "The primary key of the resource" + id: Int! @rename(attribute: "image_id") + "The component that the resource is intended for" + facet: ImageFacet! @localizedEnum + "The path of the file in storage" + path: String! + "The URL to stream the file from storage" + link: String! + anime: [Anime] @belongsToMany(type: CONNECTION) + artists: [Artist] @belongsToMany(type: CONNECTION, edgeType: ImageArtistEdge) + studios: [Studio] @belongsToMany(type: CONNECTION) + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +"Default edge to use in simple belongs to many relationships" +type ImageEdge { + node: Image! + "The date that the resource was last modified" + createdAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was deleted" + updatedAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} + +"Image Artist has an extra pivot that others image relationships don't" +type ImageArtistEdge { + node: Artist! + "Used to sort the resource images" + depth: Int @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was last modified" + createdAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was deleted" + updatedAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} + +enum ImageColumnsOrderable { + ID @enum(value: "image_id") + FACET + PATH + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of image resources given fields." + images( + facet: [ImageFacet] @in + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "ImageColumnsOrderable") + ): [Image!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Wiki/series.graphql b/app/GraphQL/Schemas/Wiki/series.graphql new file mode 100644 index 000000000..a40c8c134 --- /dev/null +++ b/app/GraphQL/Schemas/Wiki/series.graphql @@ -0,0 +1,54 @@ +""" +Represents a collection of related anime. + +For example, the Monogatari series is the collection of the Bakemonogatari anime and its related productions. +""" +type Series { + "The primary key of the resource" + id: Int! @rename(attribute: "series_id") + "The primary title of the series" + name: String! + "The URL slug & route key of the resource" + slug: String! + anime: [Anime] @belongsToMany(type: CONNECTION) + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +"Default edge to use in simple belongs to many relationships" +type SeriesEdge { + node: Series! + "The date that the resource was last modified" + createdAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was deleted" + updatedAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} + +enum SeriesColumnsOrderable { + ID @enum(value: "series_id") + NAME + SLUG + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of series resources given fields." + series( + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "SeriesColumnsOrderable") + ): [Series!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Wiki/song.graphql b/app/GraphQL/Schemas/Wiki/song.graphql new file mode 100644 index 000000000..d2ea329b6 --- /dev/null +++ b/app/GraphQL/Schemas/Wiki/song.graphql @@ -0,0 +1,54 @@ +""" +Represents the composition that accompanies an AnimeTheme. + +For example, Staple Stable is the song for the Bakemonogatari OP1 AnimeTheme. +""" +type Song { + "The primary key of the resource" + id: Int! @rename(attribute: "song_id") + "The name of the composition" + title: String + animethemes: [AnimeTheme] @hasMany + performances: [Performance] @hasMany + resources: ExternalResourceConnection! @belongsToMany(type: CONNECTION) + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +"Default edge to use in simple belongs to many relationships" +type SongEdge { + node: Song! + "The date that the resource was last modified" + createdAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was deleted" + updatedAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} + +enum SongColumnsOrderable { + ID @enum(value: "song_id") + TITLE + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of song resources given fields." + songs( + title: String @eq + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "SongColumnsOrderable") + ): [Song!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Wiki/studio.graphql b/app/GraphQL/Schemas/Wiki/studio.graphql new file mode 100644 index 000000000..4ae4b76f9 --- /dev/null +++ b/app/GraphQL/Schemas/Wiki/studio.graphql @@ -0,0 +1,56 @@ +""" +Represents a company that produces anime. + +For example, Shaft is the studio that produced the anime Bakemonogatari. +""" +type Studio { + "The primary key of the resource" + id: Int! @rename(attribute: "studio_id") + "The primary title of the studio" + name: String! + "The URL slug & route key of the resource" + slug: String! + anime: [Anime] @belongsToMany(type: CONNECTION) + images: [Image] @belongsToMany(type: CONNECTION) + resources: [ExternalResource] @belongsToMany(type: CONNECTION) + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +"Default edge to use in simple belongs to many relationships" +type StudioEdge { + node: Studio! + "The date that the resource was last modified" + createdAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was deleted" + updatedAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} + +enum StudioColumnsOrderable { + ID @enum(value: "studio_id") + NAME + SLUG + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of studio resources given fields." + studios( + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "StudioColumnsOrderable") + ): [Studio!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/Wiki/video.graphql b/app/GraphQL/Schemas/Wiki/video.graphql new file mode 100644 index 000000000..34312225f --- /dev/null +++ b/app/GraphQL/Schemas/Wiki/video.graphql @@ -0,0 +1,101 @@ +""" +Represents a WebM of an anime theme. + +For example, the video Bakemonogatari-OP1.webm represents the WebM of the Bakemonogatari OP1 theme. +""" +type Video { + "The primary key of the resource" + id: Int! @rename(attribute: "video_id") + "The basename of the file in storage" + basename: String! + "The filename of the file in storage" + filename: String! + "Does the video include subtitles of song lyrics?" + lyrics: Boolean! + "The media type of the file in storage" + mimetype: String + "Is the video creditless?" + nc: Boolean! + "The degree to which the sequence and episode content overlap" + overlap: VideoOverlap @localizedEnum + "The path of the file in storage" + path: String! + "The frame height of the file in storage" + resolution: Int + "The size of the file in storage in Bytes" + size: Int + "Where did this video come from?" + source: VideoSource @localizedEnum + "Does the video include subtitles of dialogue?" + subbed: Boolean! + "Is the video an uncensored version of a censored sequence?" + uncen: Boolean! + "The attributes used to distinguish the file within the context of a theme" + tags: String @field(resolver: "App\\GraphQL\\Resolvers\\ImplodeArrayResolver@resolve") + "The URL to stream the file from storage" + link: String! + "The number of views recorded for the resource" + viewsCount: Int! @with(relation: "viewAggregate") @field(resolver: "App\\GraphQL\\Resolvers\\ViewsCountResolver@resolve") + animethemeentries: [AnimeThemeEntry] @belongsToMany(type: CONNECTION) + videoscript: VideoScript @hasOne + "The date that the resource was created" + createdAt: DateTimeTz @rename(attribute: "created_at") + "The date that the resource was last modified" + updatedAt: DateTimeTz @rename(attribute: "updated_at") + "The date that the resource was deleted" + deletedAt: DateTimeTz @rename(attribute: "deleted_at") +} + +"Default edge to use in simple belongs to many relationships" +type VideoEdge { + node: Video! + "The date that the resource was last modified" + createdAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") + "The date that the resource was deleted" + updatedAt: DateTimeTz @field(resolver: "App\\GraphQL\\Resolvers\\PivotResolver@resolve") +} + +enum VideoColumnsOrderable { + ID @enum(value: "video_id") + FILENAME + BASENAME + LYRICS + NC + OVERLAP + RESOLUTION + SIZE + SOURCE + SUBBED + TAGS + UNCEN + CREATED_AT + UPDATED_AT + DELETED_AT +} + +extend type Query { + "Returns a listing of video resources given fields." + videos( + isLyrics: Boolean @eq + isNc: Boolean @eq + isSubbed: Boolean @eq + isUncen: Boolean @eq + overlap: [VideoOverlap] @in + source: [VideoSource] @in + resolution_in: [Int] @in(key: "resolution") + resolution_lesser: Int @where(key: "resolution", operator: "<") + resolution_greater: Int @where(key: "resolution", operator: ">") + size_lesser: Int @where(key: "size", operator: "<") + size_greater: Int @where(key: "size", operator: ">") + createdAt_lesser: DateTimeTz @where(key: "created_at", operator: "<") + createdAt_greater: DateTimeTz @where(key: "created_at", operator: ">") + updatedAt_lesser: DateTimeTz @where(key: "updated_at", operator: "<") + updatedAt_greater: DateTimeTz @where(key: "updated_at", operator: ">") + deletedAt_lesser: DateTimeTz @where(key: "deleted_at", operator: "<") + deletedAt_greater: DateTimeTz @where(key: "deleted_at", operator: ">") + orderBy: _ @orderBy(columnsEnum: "VideoColumnsOrderable") + ): [Video!]! + @canModel(ability: "viewAny", injectArgs: true) + @softDeletes + @paginate +} \ No newline at end of file diff --git a/app/GraphQL/Schemas/schema.graphql b/app/GraphQL/Schemas/schema.graphql new file mode 100644 index 000000000..22b6c1d59 --- /dev/null +++ b/app/GraphQL/Schemas/schema.graphql @@ -0,0 +1,47 @@ +"A datetime and timezone string in ISO 8601 format `Y-m-dTH:i:sP`, e.g. `2020-04-20T13:53:12+02:00`." +scalar DateTimeTz @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTimeTz") + +"Indicates what fields are available at the top level of a query operation." +type Query @middleware(class: "App\\GraphQL\\Middleware\\MaxCount") { + "Returns a listing of resources that match a given search term." + search(search: String @search, perPage: Int, page: Int): Search! + @field(resolver: "App\\GraphQL\\Queries\\Search") +} + +type Search { + anime: [Anime!]! + artists: [Artist!]! + animethemes: [AnimeTheme!]! + playlists: [Playlist!]! + series: [Series!]! + songs: [Song!]! + studios: [Studio!]! + videos: [Video!]! +} + +type MessageResponse { + message: String +} + +input WhereHasConditions { + relation: String! + where: [WhereConditions!] + AND: [WhereHasConditions!] + OR: [WhereHasConditions!] +} + +#import Admin/*.graphql +#import Auth/*.graphql +#import Document/*.graphql +#import List/*.graphql +#import List/External/*.graphql +#import List/Playlist/*.graphql +#import User/*.graphql +#import Wiki/*.graphql +#import Wiki/Anime/*.graphql +#import Wiki/Anime/Theme/*.graphql +#import Wiki/Song/*.graphql +#import Wiki/Video/*.graphql + +#import Pivot/List/*.graphql +#import Pivot/Wiki/*.graphql \ No newline at end of file diff --git a/app/GraphQL/Types/Definition/Hidden.php b/app/GraphQL/Types/Definition/Hidden.php new file mode 100644 index 000000000..942651ccb --- /dev/null +++ b/app/GraphQL/Types/Definition/Hidden.php @@ -0,0 +1,15 @@ + + */ + protected string $enumClass; + + public function __construct( + string $enumClass, + ?string $name = null, + ?string $description = null, + ?EnumTypeDefinitionNode $astNode = null, + ?array $extensionASTNodes = null + ) { + $this->enumClass = $enumClass; + $reflection = new \ReflectionEnum($enumClass); + + $enumDefinitions = []; + foreach ($enumClass::cases() as $case) { + if ($this->hidden($reflection->getCase($case->name))) { + continue; + } + $enumDefinitions[$case->name] = [ + 'value' => $case->value, + 'description' => $this->extractDescription($reflection->getCase($case->name)), + 'deprecationReason' => $this->deprecationReason($reflection->getCase($case->name)), + ]; + } + + parent::__construct([ + 'name' => $name ?? $this->baseName($enumClass), + 'values' => $enumDefinitions, + 'description' => $description ?? $this->extractDescription($reflection), + 'astNode' => $astNode, + 'extensionASTNodes' => $extensionASTNodes, + ]); + } + + /** + * @param mixed $value + * @return string + */ + public function serialize($value): string + { + return $value; + } + + /** + * @param class-string $class + * @return string + */ + protected function baseName(string $class): string + { + $parts = explode('\\', $class); + + return end($parts); + } + + /** + * @param ReflectionClassConstant|ReflectionClass $reflection + * @return string|null + * + * @throws Exception + */ + protected function extractDescription(ReflectionClassConstant|ReflectionClass $reflection): ?string + { + $attributes = $reflection->getAttributes(Description::class); + + if (count($attributes) === 1) { + return $attributes[0]->newInstance()->description; + } + + if (count($attributes) > 1) { + throw new Exception(self::MULTIPLE_DESCRIPTIONS_DISALLOWED); + } + + $comment = $reflection->getDocComment(); + $unpadded = PhpDoc::unpad($comment); + + return PhpDoc::unwrap($unpadded); + } + + /** + * @param ReflectionClassConstant $reflection + * @return string|null + * + * @throws Exception + */ + protected function deprecationReason(ReflectionClassConstant $reflection): ?string + { + $attributes = $reflection->getAttributes(Deprecated::class); + + if (count($attributes) === 1) { + return $attributes[0]->newInstance()->reason; + } + + if (count($attributes) > 1) { + throw new Exception(self::MULTIPLE_DEPRECATIONS_DISALLOWED); + } + + return null; + } + + /** + * Determine whether the enum case should be hidden. + * + * @param ReflectionClassConstant $reflection + * @return bool + * + * @throws Exception + */ + protected function hidden(ReflectionClassConstant $reflection): bool + { + $attributes = $reflection->getAttributes(Hidden::class); + + if (count($attributes) === 1) { + return $attributes[0]->newInstance()->hidden; + } + + if (count($attributes) > 1) { + throw new Exception(self::MULTIPLE_DESCRIPTIONS_DISALLOWED); + } + + return false; + } +} diff --git a/app/GraphQL/Validators/Mutation/List/CreatePlaylistValidator.php b/app/GraphQL/Validators/Mutation/List/CreatePlaylistValidator.php new file mode 100644 index 000000000..eec6e2602 --- /dev/null +++ b/app/GraphQL/Validators/Mutation/List/CreatePlaylistValidator.php @@ -0,0 +1,44 @@ +> + */ + public function rules(): array + { + return [ + Playlist::ATTRIBUTE_NAME => [ + 'required', + 'string', + 'max:192', + new ModerationRule(), + ], + Playlist::ATTRIBUTE_DESCRIPTION => [ + 'nullable', + 'string', + 'max:1000', + new ModerationRule(), + ], + Playlist::ATTRIBUTE_VISIBILITY => [ + 'required', + new Enum(PlaylistVisibility::class), + ], + ]; + } +} diff --git a/app/GraphQL/Validators/Mutation/List/Playlist/CreatePlaylistTrackValidator.php b/app/GraphQL/Validators/Mutation/List/Playlist/CreatePlaylistTrackValidator.php new file mode 100644 index 000000000..e6859acaf --- /dev/null +++ b/app/GraphQL/Validators/Mutation/List/Playlist/CreatePlaylistTrackValidator.php @@ -0,0 +1,65 @@ +> + */ + public function rules(): array + { + $playlistHashid = $this->arg('playlist'); + $playlist = Playlist::query()->firstWhere(Playlist::ATTRIBUTE_HASHID, $playlistHashid); + + return [ + PlaylistTrack::ATTRIBUTE_ENTRY => [ + 'required', + 'integer', + Rule::exists(AnimeThemeEntry::class, AnimeThemeEntry::ATTRIBUTE_ID), + Rule::exists(AnimeThemeEntryVideo::class, AnimeThemeEntryVideo::ATTRIBUTE_ENTRY) + ->where(AnimeThemeEntryVideo::ATTRIBUTE_VIDEO, $this->arg(PlaylistTrack::ATTRIBUTE_VIDEO)), + ], + PlaylistTrack::RELATION_NEXT => [ + 'sometimes', + 'required', + 'string', + Str::of('prohibits:')->append(PlaylistTrack::RELATION_PREVIOUS)->__toString(), + Rule::exists(PlaylistTrack::class, HasHashids::ATTRIBUTE_HASHID) + ->where(PlaylistTrack::ATTRIBUTE_PLAYLIST, $playlist?->getKey()), + ], + PlaylistTrack::RELATION_PREVIOUS => [ + 'sometimes', + 'required', + 'string', + Str::of('prohibits:')->append(PlaylistTrack::RELATION_NEXT)->__toString(), + Rule::exists(PlaylistTrack::class, HasHashids::ATTRIBUTE_HASHID) + ->where(PlaylistTrack::ATTRIBUTE_PLAYLIST, $playlist?->getKey()), + ], + PlaylistTrack::ATTRIBUTE_VIDEO => [ + 'required', + 'integer', + Rule::exists(Video::class, Video::ATTRIBUTE_ID), + Rule::exists(AnimeThemeEntryVideo::class, AnimeThemeEntryVideo::ATTRIBUTE_VIDEO) + ->where(AnimeThemeEntryVideo::ATTRIBUTE_ENTRY, $this->arg(PlaylistTrack::ATTRIBUTE_ENTRY)), + ], + ]; + } +} diff --git a/app/GraphQL/Validators/Mutation/List/Playlist/UpdatePlaylistTrackValidator.php b/app/GraphQL/Validators/Mutation/List/Playlist/UpdatePlaylistTrackValidator.php new file mode 100644 index 000000000..d7d8c2513 --- /dev/null +++ b/app/GraphQL/Validators/Mutation/List/Playlist/UpdatePlaylistTrackValidator.php @@ -0,0 +1,84 @@ +> + */ + public function rules(): array + { + $playlistHashid = $this->arg('playlist'); + $hashid = $this->arg(PlaylistTrack::ATTRIBUTE_HASHID); + $entryId = $this->arg(PlaylistTrack::ATTRIBUTE_ENTRY); + $videoId = $this->arg(PlaylistTrack::ATTRIBUTE_VIDEO); + + $playlist = Playlist::query()->firstWhere(Playlist::ATTRIBUTE_HASHID, $playlistHashid); + $track = PlaylistTrack::query()->firstWhere(PlaylistTrack::ATTRIBUTE_HASHID, $hashid); + + return [ + PlaylistTrack::ATTRIBUTE_ENTRY => [ + 'sometimes', + 'required', + 'integer', + Rule::exists(AnimeThemeEntry::class, AnimeThemeEntry::ATTRIBUTE_ID), + Rule::when( + ! empty($videoId), + [ + Rule::exists(AnimeThemeEntryVideo::class, AnimeThemeEntryVideo::ATTRIBUTE_ENTRY) + ->where(AnimeThemeEntryVideo::ATTRIBUTE_VIDEO, $videoId), + ] + ), + ], + PlaylistTrack::RELATION_NEXT => [ + 'sometimes', + 'required', + 'string', + Str::of('prohibits:')->append(PlaylistTrack::RELATION_PREVIOUS)->__toString(), + Rule::exists(PlaylistTrack::class, HasHashids::ATTRIBUTE_HASHID) + ->where(PlaylistTrack::ATTRIBUTE_PLAYLIST, $playlist?->getKey()) + ->whereNot(PlaylistTrack::ATTRIBUTE_ID, $track?->getKey()), + ], + PlaylistTrack::RELATION_PREVIOUS => [ + 'sometimes', + 'required', + 'string', + Str::of('prohibits:')->append(PlaylistTrack::RELATION_NEXT)->__toString(), + Rule::exists(PlaylistTrack::class, HasHashids::ATTRIBUTE_HASHID) + ->where(PlaylistTrack::ATTRIBUTE_PLAYLIST, $playlist?->getKey()) + ->whereNot(PlaylistTrack::ATTRIBUTE_ID, $track?->getKey()), + ], + PlaylistTrack::ATTRIBUTE_VIDEO => [ + 'sometimes', + 'required', + 'integer', + Rule::exists(Video::class, Video::ATTRIBUTE_ID), + Rule::when( + ! empty($entryId), + [ + Rule::exists(AnimeThemeEntryVideo::class, AnimeThemeEntryVideo::ATTRIBUTE_VIDEO) + ->where(AnimeThemeEntryVideo::ATTRIBUTE_ENTRY, $entryId), + ] + ), + ], + ]; + } +} diff --git a/app/GraphQL/Validators/Mutation/List/UpdatePlaylistValidator.php b/app/GraphQL/Validators/Mutation/List/UpdatePlaylistValidator.php new file mode 100644 index 000000000..3d79862ce --- /dev/null +++ b/app/GraphQL/Validators/Mutation/List/UpdatePlaylistValidator.php @@ -0,0 +1,46 @@ +> + */ + public function rules(): array + { + return [ + Playlist::ATTRIBUTE_NAME => [ + 'sometimes', + 'required', + 'string', + 'max:192', + new ModerationRule(), + ], + Playlist::ATTRIBUTE_DESCRIPTION => [ + 'nullable', + 'string', + 'max:1000', + new ModerationRule(), + ], + Playlist::ATTRIBUTE_VISIBILITY => [ + 'sometimes', + 'required', + new Enum(PlaylistVisibility::class), + ], + ]; + } +} diff --git a/app/Models/List/Playlist.php b/app/Models/List/Playlist.php index edbce2c56..18852923a 100644 --- a/app/Models/List/Playlist.php +++ b/app/Models/List/Playlist.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Models\List; use App\Concerns\Models\Service\AggregatesView; +use App\Contracts\Models\HasAggregateViews; use App\Contracts\Models\HasHashids; use App\Contracts\Models\HasImages; use App\Enums\Models\List\PlaylistVisibility; @@ -45,7 +46,7 @@ use Illuminate\Support\Collection; * * @method static PlaylistFactory factory(...$parameters) */ -class Playlist extends BaseModel implements HasHashids, Viewable, HasImages +class Playlist extends BaseModel implements HasHashids, Viewable, HasImages, HasAggregateViews { use AggregatesView; use Searchable; diff --git a/app/Models/Wiki/Audio.php b/app/Models/Wiki/Audio.php index b29e8a20b..439efdafa 100644 --- a/app/Models/Wiki/Audio.php +++ b/app/Models/Wiki/Audio.php @@ -6,6 +6,7 @@ namespace App\Models\Wiki; use App\Concerns\Models\Reportable; use App\Concerns\Models\Service\AggregatesView; +use App\Contracts\Models\HasAggregateViews; use App\Contracts\Models\Streamable; use App\Events\Wiki\Audio\AudioCreated; use App\Events\Wiki\Audio\AudioDeleted; @@ -34,7 +35,7 @@ use Illuminate\Support\Collection; * * @method static AudioFactory factory(...$parameters) */ -class Audio extends BaseModel implements Streamable, Viewable +class Audio extends BaseModel implements Streamable, Viewable, HasAggregateViews { use AggregatesView; use InteractsWithViews; diff --git a/app/Models/Wiki/Video.php b/app/Models/Wiki/Video.php index f3f43489e..62d67d05b 100644 --- a/app/Models/Wiki/Video.php +++ b/app/Models/Wiki/Video.php @@ -6,6 +6,7 @@ namespace App\Models\Wiki; use App\Concerns\Models\Reportable; use App\Concerns\Models\Service\AggregatesView; +use App\Contracts\Models\HasAggregateViews; use App\Contracts\Models\Streamable; use App\Enums\Models\List\PlaylistVisibility; use App\Enums\Models\Wiki\VideoOverlap; @@ -61,7 +62,7 @@ use Illuminate\Support\Collection; * * @method static VideoFactory factory(...$parameters) */ -class Video extends BaseModel implements Streamable, Viewable +class Video extends BaseModel implements Streamable, Viewable, HasAggregateViews { use AggregatesView; use Reportable; diff --git a/app/Policies/Auth/RolePolicy.php b/app/Policies/Auth/RolePolicy.php index 3e98c1eae..7ca9dabe4 100644 --- a/app/Policies/Auth/RolePolicy.php +++ b/app/Policies/Auth/RolePolicy.php @@ -24,7 +24,7 @@ class RolePolicy extends BasePolicy */ public function viewAny(?User $user): bool { - return $user->can(CrudPermission::VIEW->format(Role::class)); + return $user !== null && $user->can(CrudPermission::VIEW->format(Role::class)); } /** @@ -36,7 +36,7 @@ class RolePolicy extends BasePolicy */ public function view(?User $user, Model $role): bool { - return $user->can(CrudPermission::VIEW->format(Role::class)); + return $user !== null && $user->can(CrudPermission::VIEW->format(Role::class)); } /** diff --git a/app/Policies/BasePolicy.php b/app/Policies/BasePolicy.php index e0e06c5ee..a1fdf0b32 100644 --- a/app/Policies/BasePolicy.php +++ b/app/Policies/BasePolicy.php @@ -11,6 +11,7 @@ use App\Models\BaseModel; use Filament\Facades\Filament; use Illuminate\Auth\Access\HandlesAuthorization; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Arr; use Illuminate\Support\Str; /** @@ -146,4 +147,28 @@ abstract class BasePolicy { return $user->can(ExtendedCrudPermission::RESTORE->format(static::getModel())); } + + /** + * Resolve the model given the context. + * + * @param Model|array $args + * @param string $routeSlug + * @param string|null $requestRoute + * @return Model|null + */ + protected function resolveGraphqlModel(Model|array $args, string $routeSlug, ?string $requestRoute = null): ?Model + { + // GraphQL context. + if (is_array($args)) { + return Arr::get($args, $routeSlug); + } + + // REST API context when it needs to retrieve a model within route. + if (is_string($requestRoute)) { + return request()->route($requestRoute); + } + + // REST API context. + return $args; + } } diff --git a/app/Policies/List/Playlist/PlaylistTrackPolicy.php b/app/Policies/List/Playlist/PlaylistTrackPolicy.php index a7c48e227..309725be0 100644 --- a/app/Policies/List/Playlist/PlaylistTrackPolicy.php +++ b/app/Policies/List/Playlist/PlaylistTrackPolicy.php @@ -17,7 +17,7 @@ use Filament\Facades\Filament; use Illuminate\Database\Eloquent\Model; /** - * Class TrackPolicy. + * Class PlaylistTrackPolicy. */ class PlaylistTrackPolicy extends BasePolicy { @@ -138,4 +138,4 @@ class PlaylistTrackPolicy extends BasePolicy return $track->trashed() && $playlist?->user()->is($user) && $user->can(ExtendedCrudPermission::RESTORE->format(PlaylistTrack::class)); } -} +} \ No newline at end of file diff --git a/app/Policies/List/PlaylistPolicy.php b/app/Policies/List/PlaylistPolicy.php index fae448f34..58fc2fb34 100644 --- a/app/Policies/List/PlaylistPolicy.php +++ b/app/Policies/List/PlaylistPolicy.php @@ -188,4 +188,4 @@ class PlaylistPolicy extends BasePolicy && $user->can(CrudPermission::DELETE->format(Playlist::class)) && $user->can(CrudPermission::DELETE->format(Image::class)); } -} +} \ No newline at end of file diff --git a/app/Providers/GraphqlServiceProvider.php b/app/Providers/GraphqlServiceProvider.php new file mode 100644 index 000000000..b44e0082e --- /dev/null +++ b/app/Providers/GraphqlServiceProvider.php @@ -0,0 +1,97 @@ +bootModels(); + $this->bootEnums(); + } + + /** + * Boot the model namespaces so schema can find them. + * + * @return void + */ + protected function bootModels(): void + { + $modelNamespaces = Config::get('lighthouse.namespaces.models', []); + + $modelsBasePaths = [ + 'App\\Models' => app_path('Models'), + 'App\\Pivots' => app_path('Pivots'), + ]; + + foreach ($modelsBasePaths as $baseNamespace => $basePath) { + foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basePath)) as $file) { + if ($file->isDir() || $file->getExtension() !== 'php') { + continue; + } + + $relativePath = str_replace($basePath . DIRECTORY_SEPARATOR, '', $file->getPath()); + $relativeNamespace = $relativePath ? '\\' . str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath) : ''; + + $namespace = $baseNamespace . $relativeNamespace; + + if (!in_array($namespace, $modelNamespaces)) { + $modelNamespaces[] = $namespace; + } + } + } + + Config::set('lighthouse.namespaces.models', $modelNamespaces); + } + + /** + * Register the enums to use as type. + * + * @return void + */ + protected function bootEnums(): void + { + $typeRegistry = app(TypeRegistry::class); + $typeRegistry->register(new EnumType(ExternalEntryWatchStatus::class)); + $typeRegistry->register(new EnumType(ExternalProfileSite::class)); + $typeRegistry->register(new EnumType(ExternalProfileVisibility::class)); + $typeRegistry->register(new EnumType(PlaylistVisibility::class)); + $typeRegistry->register(new EnumType(AnimeMediaFormat::class)); + $typeRegistry->register(new EnumType(AnimeSeason::class)); + $typeRegistry->register(new EnumType(AnimeSynonymType::class)); + $typeRegistry->register(new EnumType(ImageFacet::class)); + $typeRegistry->register(new EnumType(ResourceSite::class)); + $typeRegistry->register(new EnumType(ThemeType::class)); + $typeRegistry->register(new EnumType(VideoOverlap::class)); + $typeRegistry->register(new EnumType(VideoSource::class)); + } +} diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index cce03cb0b..066d4719b 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -9,7 +9,6 @@ use App\Constants\Config\AudioConstants; use App\Constants\Config\DumpConstants; use App\Constants\Config\VideoConstants; use App\Enums\Auth\SpecialPermission; -use App\Http\Middleware\Auth\Authenticate; use App\Models\Auth\User; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; @@ -33,6 +32,25 @@ class RouteServiceProvider extends ServiceProvider */ public function boot(): void { + RateLimiter::for('graphql', function (Request $request) { + $user = Auth::user(); + $ip = $request->ip(); + $forwardedIp = $request->header('x-forwarded-ip'); + + // (If request is from client and no forwarded ip) or (the user logged in has permission to bypass API rate limiting) + /** @phpstan-ignore-next-line */ + if (($ip === '127.0.0.1' && !$forwardedIp) || ($user instanceof User && $user->can(SpecialPermission::BYPASS_GRAPHQL_RATE_LIMITER->value))) { + return Limit::none(); + } + + // Check if request is from client to prevent users from using forwarded ip + if ($ip === '127.0.0.1' && $forwardedIp) { + $ip = $forwardedIp; + } + + return Limit::perMinute(80)->by(Auth::check() ? Auth::id() : $ip); + }); + RateLimiter::for('api', function (Request $request) { $user = $request->user('sanctum'); $ip = $request->ip(); diff --git a/composer.json b/composer.json index 640d7a0d1..466de6612 100644 --- a/composer.json +++ b/composer.json @@ -47,6 +47,9 @@ "laravel/tinker": "^2.10.1", "league/flysystem-aws-s3-v3": "^3.29", "leandrocfe/filament-apex-charts": "^3.2.0", + "mll-lab/graphql-php-scalars": "^6.4", + "mll-lab/laravel-graphiql": "^3.3", + "nuwave/lighthouse": "^6.54", "propaganistas/laravel-disposable-email": "^2.4.13", "spatie/db-dumper": "^3.8.0", "spatie/laravel-permission": "^6.16.0", diff --git a/composer.lock b/composer.lock index e7fac9404..eb6dd4079 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c773da25a8dfe9e9c6dff69ace159216", + "content-hash": "ef1715326dcb579b16f809b013b40dc8", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -3330,6 +3330,48 @@ }, "time": "2023-02-23T15:00:54+00:00" }, + { + "name": "haydenpierce/class-finder", + "version": "0.5.3", + "source": { + "type": "git", + "url": "git@gitlab.com:hpierce1102/ClassFinder.git", + "reference": "40703445c18784edcc6411703e7c3869af11ec8c" + }, + "dist": { + "type": "zip", + "url": "https://gitlab.com/api/v4/projects/hpierce1102%2FClassFinder/repository/archive.zip?sha=40703445c18784edcc6411703e7c3869af11ec8c", + "reference": "40703445c18784edcc6411703e7c3869af11ec8c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.3" + }, + "require-dev": { + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "~9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "HaydenPierce\\ClassFinder\\": "src/", + "HaydenPierce\\ClassFinder\\UnitTest\\": "test/unit" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Hayden Pierce", + "email": "hayden@haydenpierce.com" + } + ], + "description": "A library that can provide of a list of classes in a given namespace", + "time": "2023-06-18T17:43:01+00:00" + }, { "name": "iamcal/sql-parser", "version": "v0.6", @@ -3486,6 +3528,64 @@ }, "time": "2025-04-01T14:41:56+00:00" }, + { + "name": "laragraph/utils", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/laragraph/utils.git", + "reference": "035c92b37f40c6b51027e76f90ef25d9a9458c56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laragraph/utils/zipball/035c92b37f40c6b51027e76f90ef25d9a9458c56", + "reference": "035c92b37f40c6b51027e76f90ef25d9a9458c56", + "shasum": "" + }, + "require": { + "illuminate/contracts": "~5.6.0 || ~5.7.0 || ~5.8.0 || ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12", + "illuminate/http": "~5.6.0 || ~5.7.0 || ~5.8.0 || ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12", + "php": "^7.2 || ^8", + "thecodingmachine/safe": "^1.1 || ^2 || ^3", + "webonyx/graphql-php": "^0.13.2 || ^14 || ^15" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.11", + "jangregor/phpstan-prophecy": "^1", + "mll-lab/php-cs-fixer-config": "^4", + "orchestra/testbench": "~3.6.0 || ~3.7.0 || ~3.8.0 || ~3.9.0 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 || ^10 || ^10.x-dev", + "phpstan/extension-installer": "^1", + "phpstan/phpstan": "^1", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9 || ^10.5 || ^11", + "thecodingmachine/phpstan-safe-rule": "^1.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laragraph\\Utils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Benedikt Franke", + "email": "benedikt@franke.tech" + } + ], + "description": "Utilities for using GraphQL with Laravel", + "homepage": "https://github.com/laragraph/utils", + "support": { + "issues": "https://github.com/laragraph/utils/issues", + "source": "https://github.com/laragraph/utils" + }, + "time": "2025-02-12T13:19:02+00:00" + }, { "name": "larastan/larastan", "version": "v3.3.1", @@ -5421,6 +5521,133 @@ }, "time": "2024-03-31T07:05:07+00:00" }, + { + "name": "mll-lab/graphql-php-scalars", + "version": "v6.4.0", + "source": { + "type": "git", + "url": "https://github.com/mll-lab/graphql-php-scalars.git", + "reference": "664ce3de2f2fb0c57f9fc127d5c75b4601290ae1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mll-lab/graphql-php-scalars/zipball/664ce3de2f2fb0c57f9fc127d5c75b4601290ae1", + "reference": "664ce3de2f2fb0c57f9fc127d5c75b4601290ae1", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.17 || ^3 || ^4", + "ext-json": "*", + "php": "^8", + "spatie/regex": "^1.4 || ^2 || ^3", + "thecodingmachine/safe": "^1.3 || ^2 || ^3", + "webonyx/graphql-php": "^15" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.16", + "mll-lab/php-cs-fixer-config": "^5", + "phpstan/extension-installer": "^1", + "phpstan/phpstan": "^1", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1", + "phpunit/phpunit": "^9 || ^10", + "symfony/var-dumper": "^5.4 || ^6", + "thecodingmachine/phpstan-safe-rule": "^1.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "MLL\\GraphQLScalars\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Benedikt Franke", + "email": "benedikt@franke.tech" + } + ], + "description": "A collection of custom scalar types for usage with https://github.com/webonyx/graphql-php", + "keywords": [ + "graphql", + "php" + ], + "support": { + "issues": "https://github.com/mll-lab/graphql-php-scalars/issues", + "source": "https://github.com/mll-lab/graphql-php-scalars/tree/v6.4.0" + }, + "time": "2025-02-14T11:22:27+00:00" + }, + { + "name": "mll-lab/laravel-graphiql", + "version": "v3.3.2", + "source": { + "type": "git", + "url": "https://github.com/mll-lab/laravel-graphiql.git", + "reference": "ddad2580094fc0ea68065546ada07b3eabe02baa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mll-lab/laravel-graphiql/zipball/ddad2580094fc0ea68065546ada07b3eabe02baa", + "reference": "ddad2580094fc0ea68065546ada07b3eabe02baa", + "shasum": "" + }, + "require": { + "illuminate/console": "^9 || ^10 || ^11 || ^12", + "illuminate/contracts": "^9 || ^10 || ^11 || ^12", + "illuminate/support": "^9 || ^10 || ^11 || ^12", + "php": "^8" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.45", + "larastan/larastan": "^2.9.14 || ^3.1", + "laravel/framework": "^9 || ^10 || ^11 || ^12", + "mll-lab/php-cs-fixer-config": "^5.10", + "orchestra/testbench": "^7.52 || ^8.33 || ^9.11 || ^10", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.20 || ^2.1.7", + "phpstan/phpstan-phpunit": "^1.4.2 || ^2.0.6", + "phpunit/phpunit": "^9.6.22 || ^10.5.45 || ^11.5.15 || ^12.0.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "MLL\\GraphiQL\\GraphiQLServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "MLL\\GraphiQL\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Benedikt Franke", + "email": "benedikt@franke.tech" + } + ], + "description": "Easily integrate GraphiQL into your Laravel project", + "keywords": [ + "graphiql", + "graphql", + "laravel" + ], + "support": { + "issues": "https://github.com/mll-lab/laravel-graphiql/issues", + "source": "https://github.com/mll-lab/laravel-graphiql/tree/v3.3.2" + }, + "time": "2025-04-01T08:12:53+00:00" + }, { "name": "monolog/monolog", "version": "3.9.0", @@ -5989,6 +6216,137 @@ ], "time": "2024-11-21T10:39:51+00:00" }, + { + "name": "nuwave/lighthouse", + "version": "v6.54.0", + "source": { + "type": "git", + "url": "https://github.com/nuwave/lighthouse.git", + "reference": "fe37f79bf530ab10ac8b57e0f2958b70236322f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nuwave/lighthouse/zipball/fe37f79bf530ab10ac8b57e0f2958b70236322f2", + "reference": "fe37f79bf530ab10ac8b57e0f2958b70236322f2", + "shasum": "" + }, + "require": { + "ext-json": "*", + "haydenpierce/class-finder": "^0.4 || ^0.5", + "illuminate/auth": "^9 || ^10 || ^11 || ^12", + "illuminate/bus": "^9 || ^10 || ^11 || ^12", + "illuminate/contracts": "^9 || ^10 || ^11 || ^12", + "illuminate/http": "^9 || ^10 || ^11 || ^12", + "illuminate/pagination": "^9 || ^10 || ^11 || ^12", + "illuminate/queue": "^9 || ^10 || ^11 || ^12", + "illuminate/routing": "^9 || ^10 || ^11 || ^12", + "illuminate/support": "^9 || ^10 || ^11 || ^12", + "illuminate/validation": "^9 || ^10 || ^11 || ^12", + "laragraph/utils": "^1.5 || ^2", + "php": "^8", + "thecodingmachine/safe": "^1 || ^2 || ^3", + "webonyx/graphql-php": "^15" + }, + "require-dev": { + "algolia/algoliasearch-client-php": "^3", + "bensampo/laravel-enum": "^5 || ^6", + "dms/phpunit-arraysubset-asserts": "^0.4 || ^0.5 || dev-add-phpunit-11-support", + "ergebnis/composer-normalize": "^2.2.2", + "fakerphp/faker": "^1.21", + "google/protobuf": "^3.21", + "larastan/larastan": "^2.9.14 || ^3.0.4", + "laravel/framework": "^9 || ^10 || ^11 || ^12", + "laravel/legacy-factories": "^1.1.1", + "laravel/pennant": "^1", + "laravel/scout": "^8 || ^9 || ^10", + "mattiasgeniar/phpunit-query-count-assertions": "^1.1", + "mll-lab/graphql-php-scalars": "^6", + "mll-lab/php-cs-fixer-config": "^5", + "mockery/mockery": "^1.5", + "nesbot/carbon": "^2.62.1 || ^3.8.4", + "orchestra/testbench": "^7.50 || ^8.32 || ^9.10 || ^10.1", + "phpbench/phpbench": "^1.2.6", + "phpstan/extension-installer": "^1", + "phpstan/phpstan": "^1.12.18 || ^2", + "phpstan/phpstan-mockery": "^1.1.3 || ^2", + "phpstan/phpstan-phpunit": "^1.1.1 || ^2", + "phpunit/phpunit": "^9.6.4 || ^10 || ^11", + "predis/predis": "^1.1 || ^2.1", + "pusher/pusher-php-server": "^5 || ^6 || ^7.0.2", + "rector/rector": "^1 || ^2", + "thecodingmachine/phpstan-safe-rule": "^1.2" + }, + "suggest": { + "ext-protobuf": "Improve protobuf serialization performance (used for tracing)", + "google/protobuf": "Required when using the tracing driver federated-tracing", + "laravel/pennant": "Required for the @feature directive", + "laravel/scout": "Required for the @search directive", + "mll-lab/graphql-php-scalars": "Useful scalar types, required for @whereConditions", + "mll-lab/laravel-graphiql": "A graphical interactive in-browser GraphQL IDE - integrated with Laravel", + "pusher/pusher-php-server": "Required when using the Pusher Subscriptions driver" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Nuwave\\Lighthouse\\LighthouseServiceProvider", + "Nuwave\\Lighthouse\\Async\\AsyncServiceProvider", + "Nuwave\\Lighthouse\\Auth\\AuthServiceProvider", + "Nuwave\\Lighthouse\\Bind\\BindServiceProvider", + "Nuwave\\Lighthouse\\Cache\\CacheServiceProvider", + "Nuwave\\Lighthouse\\GlobalId\\GlobalIdServiceProvider", + "Nuwave\\Lighthouse\\OrderBy\\OrderByServiceProvider", + "Nuwave\\Lighthouse\\Pagination\\PaginationServiceProvider", + "Nuwave\\Lighthouse\\SoftDeletes\\SoftDeletesServiceProvider", + "Nuwave\\Lighthouse\\Testing\\TestingServiceProvider", + "Nuwave\\Lighthouse\\Validation\\ValidationServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Nuwave\\Lighthouse\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Moore", + "email": "chris@nuwavecommerce.com", + "homepage": "https://www.nuwavecommerce.com" + }, + { + "name": "Benedikt Franke", + "email": "benedikt@franke.tech", + "homepage": "https://franke.tech" + } + ], + "description": "A framework for serving GraphQL from Laravel", + "homepage": "https://lighthouse-php.com", + "keywords": [ + "graphql", + "laravel", + "laravel-graphql" + ], + "support": { + "issues": "https://github.com/nuwave/lighthouse/issues", + "source": "https://github.com/nuwave/lighthouse" + }, + "funding": [ + { + "url": "https://github.com/spawnia", + "type": "github" + }, + { + "url": "https://www.patreon.com/lighthouse_php", + "type": "patreon" + } + ], + "time": "2025-03-25T16:18:27+00:00" + }, { "name": "open-telemetry/api", "version": "1.2.3", @@ -8261,6 +8619,69 @@ ], "time": "2025-02-28T20:29:57+00:00" }, + { + "name": "spatie/regex", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/regex.git", + "reference": "d543de2019a0068e7b80da0ba24f1c51c7469303" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/regex/zipball/d543de2019a0068e7b80da0ba24f1c51c7469303", + "reference": "d543de2019a0068e7b80da0ba24f1c51c7469303", + "shasum": "" + }, + "require": { + "php": "^8.0|^8.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Regex\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sebastian De Deyne", + "email": "sebastian@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "A sane interface for php's built in preg_* functions", + "homepage": "https://github.com/spatie/regex", + "keywords": [ + "expression", + "expressions", + "regex", + "regular", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/regex/issues", + "source": "https://github.com/spatie/regex/tree/3.1.1" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2021-11-30T21:13:59+00:00" + }, { "name": "staudenmeir/belongs-to-through", "version": "v2.17", @@ -11160,6 +11581,145 @@ }, "time": "2022-04-26T06:28:24+00:00" }, + { + "name": "thecodingmachine/safe", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "22ffad3248982a784f9870a37aeb2e522bd19645" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/22ffad3248982a784f9870a37aeb2e522bd19645", + "reference": "22ffad3248982a784f9870a37aeb2e522bd19645", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" + }, + "type": "library", + "autoload": { + "files": [ + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rnp.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v3.0.2" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2025-02-19T19:23:00+00:00" + }, { "name": "tijsverkoyen/css-to-inline-styles", "version": "v2.3.0", @@ -11497,6 +12057,80 @@ "source": "https://github.com/webmozarts/assert/tree/1.11.0" }, "time": "2022-06-03T18:03:27+00:00" + }, + { + "name": "webonyx/graphql-php", + "version": "v15.20.0", + "source": { + "type": "git", + "url": "https://github.com/webonyx/graphql-php.git", + "reference": "60feb7ad5023c0ef411efbdf9792d3df5812e28f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/60feb7ad5023c0ef411efbdf9792d3df5812e28f", + "reference": "60feb7ad5023c0ef411efbdf9792d3df5812e28f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "^7.4 || ^8" + }, + "require-dev": { + "amphp/amp": "^2.6", + "amphp/http-server": "^2.1", + "dms/phpunit-arraysubset-asserts": "dev-master", + "ergebnis/composer-normalize": "^2.28", + "friendsofphp/php-cs-fixer": "3.73.1", + "mll-lab/php-cs-fixer-config": "5.11.0", + "nyholm/psr7": "^1.5", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "2.1.8", + "phpstan/phpstan-phpunit": "2.0.4", + "phpstan/phpstan-strict-rules": "2.0.4", + "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11", + "psr/http-message": "^1 || ^2", + "react/http": "^1.6", + "react/promise": "^2.0 || ^3.0", + "rector/rector": "^2.0", + "symfony/polyfill-php81": "^1.23", + "symfony/var-exporter": "^5 || ^6 || ^7", + "thecodingmachine/safe": "^1.3 || ^2 || ^3" + }, + "suggest": { + "amphp/http-server": "To leverage async resolving with webserver on AMPHP platform", + "psr/http-message": "To use standard GraphQL server", + "react/promise": "To leverage async resolving on React PHP platform" + }, + "type": "library", + "autoload": { + "psr-4": { + "GraphQL\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP port of GraphQL reference implementation", + "homepage": "https://github.com/webonyx/graphql-php", + "keywords": [ + "api", + "graphql" + ], + "support": { + "issues": "https://github.com/webonyx/graphql-php/issues", + "source": "https://github.com/webonyx/graphql-php/tree/v15.20.0" + }, + "funding": [ + { + "url": "https://opencollective.com/webonyx-graphql-php", + "type": "open_collective" + } + ], + "time": "2025-03-21T08:45:04+00:00" } ], "packages-dev": [ diff --git a/config/app.php b/config/app.php index 2a6a9bcf7..fd1e52eae 100644 --- a/config/app.php +++ b/config/app.php @@ -214,6 +214,9 @@ return [ /* * Package Service Providers... */ + Nuwave\Lighthouse\Pennant\PennantServiceProvider::class, + Nuwave\Lighthouse\Scout\ScoutServiceProvider::class, + Nuwave\Lighthouse\WhereConditions\WhereConditionsServiceProvider::class, /* * Application Service Providers... @@ -223,6 +226,7 @@ return [ App\Providers\EventServiceProvider::class, App\Providers\FilamentPanelProvider::class, App\Providers\FortifyServiceProvider::class, + App\Providers\GraphqlServiceProvider::class, App\Providers\HorizonServiceProvider::class, App\Providers\PulseServiceProvider::class, App\Providers\RouteServiceProvider::class, diff --git a/config/graphiql.php b/config/graphiql.php new file mode 100644 index 000000000..c91bd29ec --- /dev/null +++ b/config/graphiql.php @@ -0,0 +1,66 @@ + [ + '/graphiql' => [ + 'name' => 'graphiql', + // 'middleware' => ['web'], + 'prefix' => env('GRAPHQL_PREFIX', null), + 'domain' => env('GRAPHQL_DOMAIN_NAME', env('APP_URL')), + + /* + |-------------------------------------------------------------------------- + | Default GraphQL endpoint + |-------------------------------------------------------------------------- + | + | The default endpoint that the GraphiQL UI is set to. + | It assumes you are running GraphQL on the same domain + | as GraphiQL, but can be set to any URL. + | + */ + + 'endpoint' => env('GRAPHQL_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Subscription endpoint + |-------------------------------------------------------------------------- + | + | The default subscription endpoint the GraphiQL UI uses to connect to. + | Tries to connect to the `endpoint` value if `null` as ws://{{endpoint}} + | + | Example: `ws://your-endpoint` or `wss://your-endpoint` + | + */ + + 'subscription-endpoint' => env('GRAPHIQL_SUBSCRIPTION_ENDPOINT', null), + ], + ], + + /* + |-------------------------------------------------------------------------- + | Control GraphiQL availability + |-------------------------------------------------------------------------- + | + | Control if the GraphiQL UI is accessible at all. + | This allows you to disable it in certain environments, + | for example you might not want it active in production. + | + */ + + 'enabled' => env('GRAPHIQL_ENABLED', true), +]; diff --git a/config/lighthouse.php b/config/lighthouse.php new file mode 100644 index 000000000..ca8d0e522 --- /dev/null +++ b/config/lighthouse.php @@ -0,0 +1,545 @@ + false, to disable the default route + | registration and take full control. + | + */ + + 'route' => [ + /* + * The URI the endpoint responds to, e.g. mydomain.com/graphql. + */ + 'uri' => env('GRAPHQL_PATH'), + + /* + * Lighthouse creates a named route for convenient URL generation and redirects. + */ + 'name' => 'graphql', + + /* + * Beware that middleware defined here runs before the GraphQL execution phase, + * make sure to return spec-compliant responses in case an error is thrown. + */ + 'middleware' => [ + // Rate limiting GraphQL to prevent abuse. + 'throttle:graphql', + + // GraphQL is enabled for localhost requests. + App\GraphQL\Middleware\GraphqlLocalhost::class, + + // Ensures the request is not vulnerable to cross-site request forgery. + Nuwave\Lighthouse\Http\Middleware\EnsureXHR::class, + + // Always set the `Accept: application/json` header. + Nuwave\Lighthouse\Http\Middleware\AcceptJson::class, + + // Logs in a user if they are authenticated. In contrast to Laravel's 'auth' + // middleware, this delegates auth and permission checks to the field level. + Nuwave\Lighthouse\Http\Middleware\AttemptAuthentication::class, + + // GraphQL needs to have their own policies. + App\GraphQL\Middleware\GraphQLPolicy::class, + + // Logs every incoming GraphQL query. + // Nuwave\Lighthouse\Http\Middleware\LogGraphQLQueries::class, + ], + + /* + * The `prefix`, `domain` and `where` configuration options are optional. + */ + 'prefix' => env('GRAPHQL_PREFIX', null), + 'domain' => env('GRAPHQL_DOMAIN_NAME'), + // 'where' => [], + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | The guards to use for authenticating GraphQL requests, if needed. + | Used in directives such as `@guard` or the `AttemptAuthentication` middleware. + | Falls back to the Laravel default if `null`. + | + */ + + 'guards' => ['web', 'sanctum'], + + /* + |-------------------------------------------------------------------------- + | Schema Path + |-------------------------------------------------------------------------- + | + | Path to your .graphql schema file. + | Additional schema files may be imported from within that file. + | + */ + + 'schema_path' => base_path('app/GraphQL/Schemas/schema.graphql'), + + /* + |-------------------------------------------------------------------------- + | Schema Cache + |-------------------------------------------------------------------------- + | + | A large part of schema generation consists of parsing and AST manipulation. + | This operation is very expensive, so it is highly recommended enabling + | caching of the final schema to optimize performance of large schemas. + | + */ + + 'schema_cache' => [ + /* + * Setting to true enables schema caching. + */ + 'enable' => env('LIGHTHOUSE_SCHEMA_CACHE_ENABLE', env('APP_ENV') !== 'local'), + + /* + * File path to store the lighthouse schema. + */ + 'path' => env('LIGHTHOUSE_SCHEMA_CACHE_PATH', base_path('bootstrap/cache/lighthouse-schema.php')), + ], + + /* + |-------------------------------------------------------------------------- + | Cache Directive Tags + |-------------------------------------------------------------------------- + | + | Should the `@cache` directive use a tagged cache? + | + */ + 'cache_directive_tags' => false, + + /* + |-------------------------------------------------------------------------- + | Query Cache + |-------------------------------------------------------------------------- + | + | Caches the result of parsing incoming query strings to boost performance on subsequent requests. + | + */ + + 'query_cache' => [ + /* + * Setting to true enables query caching. + */ + 'enable' => env('LIGHTHOUSE_QUERY_CACHE_ENABLE', true), + + /* + * Allows using a specific cache store, uses the app's default if set to null. + */ + 'store' => env('LIGHTHOUSE_QUERY_CACHE_STORE', null), + + /* + * Duration in seconds the query should remain cached, null means forever. + */ + 'ttl' => env('LIGHTHOUSE_QUERY_CACHE_TTL', 24 * 60 * 60), + ], + + /* + |-------------------------------------------------------------------------- + | Validation Cache + |-------------------------------------------------------------------------- + | + | Caches the result of validating queries to boost performance on subsequent requests. + | + */ + + 'validation_cache' => [ + /* + * Setting to true enables validation caching. + */ + 'enable' => env('LIGHTHOUSE_VALIDATION_CACHE_ENABLE', false), + + /* + * Allows using a specific cache store, uses the app's default if set to null. + */ + 'store' => env('LIGHTHOUSE_VALIDATION_CACHE_STORE', null), + + /* + * Duration in seconds the validation result should remain cached, null means forever. + */ + 'ttl' => env('LIGHTHOUSE_VALIDATION_CACHE_TTL', 24 * 60 * 60), + ], + + /* + |-------------------------------------------------------------------------- + | Parse source location + |-------------------------------------------------------------------------- + | + | Should the source location be included in the AST nodes resulting from query parsing? + | Setting this to `false` improves performance, but omits the key `locations` from errors, + | see https://spec.graphql.org/October2021/#sec-Errors.Error-result-format. + | + */ + + 'parse_source_location' => true, + + /* + |-------------------------------------------------------------------------- + | Namespaces + |-------------------------------------------------------------------------- + | + | These are the default namespaces where Lighthouse looks for classes to + | extend functionality of the schema. You may pass in either a string + | or an array, they are tried in order and the first match is used. + | + */ + + 'namespaces' => [ + 'models' => [], + 'queries' => 'App\\GraphQL\\Queries', + 'mutations' => 'App\\GraphQL\\Mutations', + 'subscriptions' => 'App\\GraphQL\\Subscriptions', + 'types' => 'App\\GraphQL\\Types', + 'interfaces' => 'App\\GraphQL\\Interfaces', + 'unions' => 'App\\GraphQL\\Unions', + 'scalars' => 'App\\GraphQL\\Scalars', + 'directives' => 'App\\GraphQL\\Directives', + 'validators' => 'App\\GraphQL\\Validators', + ], + + /* + |-------------------------------------------------------------------------- + | Security + |-------------------------------------------------------------------------- + | + | Control how Lighthouse handles security related query validation. + | Read more at https://webonyx.github.io/graphql-php/security/ + | + */ + + 'security' => [ + 'max_query_complexity' => QueryComplexity::DISABLED, + 'max_query_depth' => QueryDepth::DISABLED, + 'disable_introspection' => (bool) env('LIGHTHOUSE_SECURITY_DISABLE_INTROSPECTION', false) + ? GraphQL\Validator\Rules\DisableIntrospection::ENABLED + : GraphQL\Validator\Rules\DisableIntrospection::DISABLED, + ], + + /* + |-------------------------------------------------------------------------- + | Pagination + |-------------------------------------------------------------------------- + | + | Set defaults for the pagination features within Lighthouse, such as + | the @paginate directive, or paginated relation directives. + | + */ + + 'pagination' => [ + /* + * Allow clients to query paginated lists without specifying the amount of items. + * Setting this to `null` means clients have to explicitly ask for the count. + */ + 'default_count' => 15, + + /* + * Limit the maximum amount of items that clients can request from paginated lists. + * Setting this to `null` means the count is unrestricted. + */ + 'max_count' => 100, + ], + + /* + |-------------------------------------------------------------------------- + | Debug + |-------------------------------------------------------------------------- + | + | Control the debug level as described in https://webonyx.github.io/graphql-php/error-handling/ + | Debugging is only applied if the global Laravel debug config is set to true. + | + | When you set this value through an environment variable, use the following reference table: + | 0 => INCLUDE_NONE + | 1 => INCLUDE_DEBUG_MESSAGE + | 2 => INCLUDE_TRACE + | 3 => INCLUDE_TRACE | INCLUDE_DEBUG_MESSAGE + | 4 => RETHROW_INTERNAL_EXCEPTIONS + | 5 => RETHROW_INTERNAL_EXCEPTIONS | INCLUDE_DEBUG_MESSAGE + | 6 => RETHROW_INTERNAL_EXCEPTIONS | INCLUDE_TRACE + | 7 => RETHROW_INTERNAL_EXCEPTIONS | INCLUDE_TRACE | INCLUDE_DEBUG_MESSAGE + | 8 => RETHROW_UNSAFE_EXCEPTIONS + | 9 => RETHROW_UNSAFE_EXCEPTIONS | INCLUDE_DEBUG_MESSAGE + | 10 => RETHROW_UNSAFE_EXCEPTIONS | INCLUDE_TRACE + | 11 => RETHROW_UNSAFE_EXCEPTIONS | INCLUDE_TRACE | INCLUDE_DEBUG_MESSAGE + | 12 => RETHROW_UNSAFE_EXCEPTIONS | RETHROW_INTERNAL_EXCEPTIONS + | 13 => RETHROW_UNSAFE_EXCEPTIONS | RETHROW_INTERNAL_EXCEPTIONS | INCLUDE_DEBUG_MESSAGE + | 14 => RETHROW_UNSAFE_EXCEPTIONS | RETHROW_INTERNAL_EXCEPTIONS | INCLUDE_TRACE + | 15 => RETHROW_UNSAFE_EXCEPTIONS | RETHROW_INTERNAL_EXCEPTIONS | INCLUDE_TRACE | INCLUDE_DEBUG_MESSAGE + | + */ + + 'debug' => env('LIGHTHOUSE_DEBUG', env('APP_ENV') === 'local') ? DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE : DebugFlag::NONE, + + /* + |-------------------------------------------------------------------------- + | Error Handlers + |-------------------------------------------------------------------------- + | + | Register error handlers that receive the Errors that occur during execution + | and handle them. You may use this to log, filter or format the errors. + | The classes must implement \Nuwave\Lighthouse\Execution\ErrorHandler + | + */ + + 'error_handlers' => [ + Nuwave\Lighthouse\Execution\AuthenticationErrorHandler::class, + Nuwave\Lighthouse\Execution\AuthorizationErrorHandler::class, + Nuwave\Lighthouse\Execution\ValidationErrorHandler::class, + Nuwave\Lighthouse\Execution\ReportingErrorHandler::class, + ], + + /* + |-------------------------------------------------------------------------- + | Field Middleware + |-------------------------------------------------------------------------- + | + | Register global field middleware directives that wrap around every field. + | Execution happens in the defined order, before other field middleware. + | The classes must implement \Nuwave\Lighthouse\Support\Contracts\FieldMiddleware + | + */ + + 'field_middleware' => [ + Nuwave\Lighthouse\Schema\Directives\TrimDirective::class, + Nuwave\Lighthouse\Schema\Directives\ConvertEmptyStringsToNullDirective::class, + Nuwave\Lighthouse\Schema\Directives\SanitizeDirective::class, + Nuwave\Lighthouse\Validation\ValidateDirective::class, + Nuwave\Lighthouse\Schema\Directives\TransformArgsDirective::class, + Nuwave\Lighthouse\Schema\Directives\SpreadDirective::class, + Nuwave\Lighthouse\Schema\Directives\RenameArgsDirective::class, + Nuwave\Lighthouse\Schema\Directives\DropArgsDirective::class, + ], + + /* + |-------------------------------------------------------------------------- + | Global ID + |-------------------------------------------------------------------------- + | + | The name that is used for the global id field on the Node interface. + | When creating a Relay compliant server, this must be named "id". + | + */ + + 'global_id_field' => 'id', + + /* + |-------------------------------------------------------------------------- + | Persisted Queries + |-------------------------------------------------------------------------- + | + | Lighthouse supports Automatic Persisted Queries (APQ), compatible with the + | [Apollo implementation](https://www.apollographql.com/docs/apollo-server/performance/apq). + | You may set this flag to either process or deny these queries. + | + */ + + 'persisted_queries' => true, + + /* + |-------------------------------------------------------------------------- + | Transactional Mutations + |-------------------------------------------------------------------------- + | + | If set to true, the execution of built-in directives that mutate models + | will be wrapped in a transaction to ensure atomicity. + | The transaction is committed after the root field resolves, + | thus errors in nested fields do not cause a rollback. + | + */ + + 'transactional_mutations' => true, + + /* + |-------------------------------------------------------------------------- + | Mass Assignment Protection + |-------------------------------------------------------------------------- + | + | If set to true, mutations will use forceFill() over fill() when populating + | a model with arguments in mutation directives. Since GraphQL constrains + | allowed inputs by design, mass assignment protection is not needed. + | + */ + + 'force_fill' => true, + + /* + |-------------------------------------------------------------------------- + | Batchload Relations + |-------------------------------------------------------------------------- + | + | If set to true, relations marked with directives like @hasMany or @belongsTo + | will be optimized by combining the queries through the BatchLoader. + | + */ + + 'batchload_relations' => true, + + /* + |-------------------------------------------------------------------------- + | Shortcut Foreign Key Selection + |-------------------------------------------------------------------------- + | + | If set to true, Lighthouse will shortcut queries where the client selects only the + | foreign key pointing to a related model. Only works if the related model's primary + | key field is called exactly `id` for every type in your schema. + | + */ + + 'shortcut_foreign_key_selection' => false, + + /* + |-------------------------------------------------------------------------- + | GraphQL Subscriptions + |-------------------------------------------------------------------------- + | + | Here you can define GraphQL subscription broadcaster and storage drivers + | as well their required configuration options. + | + */ + + 'subscriptions' => [ + /* + * Determines if broadcasts should be queued by default. + */ + 'queue_broadcasts' => env('LIGHTHOUSE_QUEUE_BROADCASTS', true), + + /* + * Determines the queue to use for broadcasting queue jobs. + */ + 'broadcasts_queue_name' => env('LIGHTHOUSE_BROADCASTS_QUEUE_NAME', null), + + /* + * Default subscription storage. + * + * Any Laravel supported cache driver options are available here. + */ + 'storage' => env('LIGHTHOUSE_SUBSCRIPTION_STORAGE', 'redis'), + + /* + * Default subscription storage time to live in seconds. + * + * Indicates how long a subscription can be active before it's automatically removed from storage. + * Setting this to `null` means the subscriptions are stored forever. This may cause + * stale subscriptions to linger indefinitely in case cleanup fails for any reason. + */ + 'storage_ttl' => env('LIGHTHOUSE_SUBSCRIPTION_STORAGE_TTL', null), + + /* + * Encrypt subscription channels by prefixing their names with "private-encrypted-"? + */ + 'encrypted_channels' => env('LIGHTHOUSE_SUBSCRIPTION_ENCRYPTED', false), + + /* + * Default subscription broadcaster. + */ + 'broadcaster' => env('LIGHTHOUSE_BROADCASTER', 'pusher'), + + /* + * Subscription broadcasting drivers with config options. + */ + 'broadcasters' => [ + 'log' => [ + 'driver' => 'log', + ], + 'echo' => [ + 'driver' => 'echo', + 'connection' => env('LIGHTHOUSE_SUBSCRIPTION_REDIS_CONNECTION', 'default'), + 'routes' => Nuwave\Lighthouse\Subscriptions\SubscriptionRouter::class . '@echoRoutes', + ], + 'pusher' => [ + 'driver' => 'pusher', + 'connection' => 'pusher', + 'routes' => Nuwave\Lighthouse\Subscriptions\SubscriptionRouter::class . '@pusher', + ], + 'reverb' => [ + 'driver' => 'pusher', + 'connection' => 'reverb', + 'routes' => Nuwave\Lighthouse\Subscriptions\SubscriptionRouter::class . '@reverb', + ], + ], + + /* + * Should the subscriptions extension be excluded when the response has no subscription channel? + * This optimizes performance by sending less data, but clients must anticipate this appropriately. + */ + 'exclude_empty' => env('LIGHTHOUSE_SUBSCRIPTION_EXCLUDE_EMPTY', true), + ], + + /* + |-------------------------------------------------------------------------- + | Defer + |-------------------------------------------------------------------------- + | + | Configuration for the experimental @defer directive support. + | + */ + + 'defer' => [ + /* + * Maximum number of nested fields that can be deferred in one query. + * Once reached, remaining fields will be resolved synchronously. + * 0 means unlimited. + */ + 'max_nested_fields' => 0, + + /* + * Maximum execution time for deferred queries in milliseconds. + * Once reached, remaining fields will be resolved synchronously. + * 0 means unlimited. + */ + 'max_execution_ms' => 0, + ], + + /* + |-------------------------------------------------------------------------- + | Apollo Federation + |-------------------------------------------------------------------------- + | + | Lighthouse can act as a federated service: https://www.apollographql.com/docs/federation/federation-spec. + | + */ + + 'federation' => [ + /* + * Location of resolver classes when resolving the `_entities` field. + */ + 'entities_resolver_namespace' => 'App\\GraphQL\\Entities', + ], + + /* + |-------------------------------------------------------------------------- + | Tracing + |-------------------------------------------------------------------------- + | + | Configuration for tracing support. + | + */ + + 'tracing' => [ + /* + * Driver used for tracing. + * + * Accepts the fully qualified class name of a class that implements Nuwave\Lighthouse\Tracing\Tracing. + * Lighthouse provides: + * - Nuwave\Lighthouse\Tracing\ApolloTracing\ApolloTracing::class + * - Nuwave\Lighthouse\Tracing\FederatedTracing\FederatedTracing::class + * + * In Lighthouse v7 the default will be changed to 'Nuwave\Lighthouse\Tracing\FederatedTracing\FederatedTracing::class'. + */ + 'driver' => Nuwave\Lighthouse\Tracing\ApolloTracing\ApolloTracing::class, + ], +]; diff --git a/resources/views/vendor/filament-daterangepicker-filter/.gitkeep b/resources/views/vendor/filament-daterangepicker-filter/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/resources/views/vendor/filament-daterangepicker-filter/date-range-picker.blade.php b/resources/views/vendor/filament-daterangepicker-filter/date-range-picker.blade.php deleted file mode 100644 index 87d67f38e..000000000 --- a/resources/views/vendor/filament-daterangepicker-filter/date-range-picker.blade.php +++ /dev/null @@ -1,135 +0,0 @@ -@php - $name = $getId(); - $isPrefixInline = $isPrefixInline(); - $isSuffixInline = $isSuffixInline(); - $prefixActions = $getPrefixActions(); - $prefixIcon = $getPrefixIcon(); - $prefixLabel = $getPrefixLabel(); - $suffixActions = $getSuffixActions(); - $suffixIcon = $getSuffixIcon(); - $suffixLabel = $getSuffixLabel(); - $statePath = $getStatePath(); -@endphp - - -
-
merge($getExtraAttributes())->class(['filament-forms-date-time-picker-component relative']) }} - {{ $getExtraAlpineAttributeBag() }} - > - - - -
-
-