diff --git a/app/Concerns/GraphQL/ResolvesArguments.php b/app/Concerns/GraphQL/ResolvesArguments.php index ff940f7b3..1666916a9 100644 --- a/app/Concerns/GraphQL/ResolvesArguments.php +++ b/app/Concerns/GraphQL/ResolvesArguments.php @@ -45,7 +45,7 @@ trait ResolvesArguments return collect($fields) ->filter(fn (Field $field): bool => $field instanceof CreatableField) ->map( - fn (Field $field): Argument => new Argument($field->getColumn(), $field->baseType()) + fn (Field $field): Argument => new Argument($field->getName(), $field->baseType()) ->required($field instanceof RequiredOnCreation) ) ->flatten() @@ -63,7 +63,7 @@ trait ResolvesArguments return collect($fields) ->filter(fn (Field $field): bool => $field instanceof UpdatableField) ->map( - fn (Field $field): Argument => new Argument($field->getColumn(), $field->baseType()) + fn (Field $field): Argument => new Argument($field->getName(), $field->baseType()) ->required($field instanceof RequiredOnUpdate) ) ->flatten() diff --git a/app/Enums/Auth/Role.php b/app/Enums/Auth/Role.php index ef4f6d63c..18f7c8d67 100644 --- a/app/Enums/Auth/Role.php +++ b/app/Enums/Auth/Role.php @@ -42,7 +42,7 @@ enum Role: string /** * Get the priority value for the role. */ - public function priority(): ?int + public function priority(): int { return match ($this) { Role::ADMIN => 250000, @@ -52,7 +52,7 @@ enum Role: string Role::PATRON => 75000, Role::CONTRIBUTOR => 50000, Role::PANEL_VIEWER => 25000, - default => null, + default => 0, }; } } diff --git a/app/GraphQL/Criteria/Filter/FilterCriteria.php b/app/GraphQL/Criteria/Filter/FilterCriteria.php index 351560176..1f94ca1bc 100644 --- a/app/GraphQL/Criteria/Filter/FilterCriteria.php +++ b/app/GraphQL/Criteria/Filter/FilterCriteria.php @@ -82,7 +82,7 @@ abstract class FilterCriteria continue; } - if ($filter instanceof WhereConditionsFilter && $type instanceof EloquentType) { + if ($filter instanceof WhereConditionsFilter && $type instanceof EloquentType && $type->hasFilterableColumns()) { $criteria[] = new WhereConditionsFilterCriteria($filter, $value, $type); continue; } diff --git a/app/GraphQL/Resolvers/List/Playlist/PlaylistTrackResolver.php b/app/GraphQL/Resolvers/List/Playlist/PlaylistTrackResolver.php index 5e6aed68e..f60370c49 100644 --- a/app/GraphQL/Resolvers/List/Playlist/PlaylistTrackResolver.php +++ b/app/GraphQL/Resolvers/List/Playlist/PlaylistTrackResolver.php @@ -19,6 +19,9 @@ use Illuminate\Support\Arr; */ class PlaylistTrackResolver extends BaseResolver { + final public const string ATTRIBUTE_ENTRY = 'entryId'; + final public const string ATTRIBUTE_VIDEO = 'videoId'; + /** * @param array $args */ @@ -26,6 +29,11 @@ class PlaylistTrackResolver extends BaseResolver { $validated = $this->validated($args, CreatePlaylistTrackMutation::class); + $validated += [ + PlaylistTrack::ATTRIBUTE_ENTRY => Arr::integer($validated, 'entryId'), + PlaylistTrack::ATTRIBUTE_VIDEO => Arr::integer($validated, 'videoId'), + ]; + /** @var Playlist $playlist */ $playlist = Arr::pull($validated, 'playlist'); @@ -41,6 +49,11 @@ class PlaylistTrackResolver extends BaseResolver { $validated = $this->validated($args, UpdatePlaylistTrackMutation::class); + $validated += [ + PlaylistTrack::ATTRIBUTE_ENTRY => Arr::integer($validated, 'entryId'), + PlaylistTrack::ATTRIBUTE_VIDEO => Arr::integer($validated, 'videoId'), + ]; + /** @var PlaylistTrack $track */ $track = Arr::pull($validated, self::MODEL); diff --git a/app/GraphQL/Resolvers/User/WatchResolver.php b/app/GraphQL/Resolvers/User/WatchResolver.php new file mode 100644 index 000000000..7d7509398 --- /dev/null +++ b/app/GraphQL/Resolvers/User/WatchResolver.php @@ -0,0 +1,38 @@ + + */ +class WatchResolver extends BaseResolver +{ + final public const string ATTRIBUTE_ENTRY = 'entryId'; + final public const string ATTRIBUTE_VIDEO = 'videoId'; + + /** + * @param array $args + * @return WatchHistory + */ + public function store($root, array $args): Model + { + $validated = $this->validated($args, WatchMutation::class); + + $validated += [ + WatchHistory::ATTRIBUTE_ENTRY => Arr::integer($validated, 'entryId'), + WatchHistory::ATTRIBUTE_VIDEO => Arr::integer($validated, 'videoId'), + WatchHistory::ATTRIBUTE_USER => Auth::id(), + ]; + + return $this->storeAction->store(WatchHistory::query(), $validated); + } +} diff --git a/app/GraphQL/Schema/Enums/SortableColumns.php b/app/GraphQL/Schema/Enums/SortableColumns.php index 9bddc231c..c7d0f0466 100644 --- a/app/GraphQL/Schema/Enums/SortableColumns.php +++ b/app/GraphQL/Schema/Enums/SortableColumns.php @@ -39,9 +39,11 @@ class SortableColumns extends EnumType */ public function attributes(): array { + $typeName = $this->type->getName(); + $name = $this->pivotType instanceof PivotType - ? $this->pivotType->getName() - : $this->type->getName(); + ? $typeName.$this->pivotType->getName() + : $typeName; return [ 'name' => $name.self::SUFFIX, diff --git a/app/GraphQL/Schema/Fields/List/Playlist/PlaylistTrack/PlaylistTrackEntryIdField.php b/app/GraphQL/Schema/Fields/List/Playlist/PlaylistTrack/PlaylistTrackEntryIdField.php index beb338633..8c1c7b0b0 100644 --- a/app/GraphQL/Schema/Fields/List/Playlist/PlaylistTrack/PlaylistTrackEntryIdField.php +++ b/app/GraphQL/Schema/Fields/List/Playlist/PlaylistTrack/PlaylistTrackEntryIdField.php @@ -7,6 +7,7 @@ namespace App\GraphQL\Schema\Fields\List\Playlist\PlaylistTrack; use App\Contracts\GraphQL\Fields\CreatableField; use App\Contracts\GraphQL\Fields\RequiredOnCreation; use App\Contracts\GraphQL\Fields\UpdatableField; +use App\GraphQL\Resolvers\List\Playlist\PlaylistTrackResolver; use App\GraphQL\Schema\Fields\Field; use App\Models\List\Playlist\PlaylistTrack; use App\Models\Wiki\Anime\Theme\AnimeThemeEntry; @@ -42,7 +43,7 @@ class PlaylistTrackEntryIdField extends Field implements CreatableField, Require 'integer', Rule::exists(AnimeThemeEntry::class, AnimeThemeEntry::ATTRIBUTE_ID), Rule::exists(AnimeThemeEntryVideo::class, AnimeThemeEntryVideo::ATTRIBUTE_ENTRY) - ->where(AnimeThemeEntryVideo::ATTRIBUTE_VIDEO, Arr::get($args, PlaylistTrack::ATTRIBUTE_VIDEO)), + ->where(AnimeThemeEntryVideo::ATTRIBUTE_VIDEO, Arr::get($args, PlaylistTrackResolver::ATTRIBUTE_VIDEO)), ]; } @@ -51,7 +52,7 @@ class PlaylistTrackEntryIdField extends Field implements CreatableField, Require */ public function getUpdateRules(array $args): array { - $videoId = Arr::get($args, PlaylistTrack::ATTRIBUTE_VIDEO); + $videoId = Arr::get($args, PlaylistTrackResolver::ATTRIBUTE_VIDEO); return [ 'sometimes', diff --git a/app/GraphQL/Schema/Fields/List/Playlist/PlaylistTrack/PlaylistTrackVideoIdField.php b/app/GraphQL/Schema/Fields/List/Playlist/PlaylistTrack/PlaylistTrackVideoIdField.php index 95ed4e557..b5ddcfb19 100644 --- a/app/GraphQL/Schema/Fields/List/Playlist/PlaylistTrack/PlaylistTrackVideoIdField.php +++ b/app/GraphQL/Schema/Fields/List/Playlist/PlaylistTrack/PlaylistTrackVideoIdField.php @@ -7,6 +7,7 @@ namespace App\GraphQL\Schema\Fields\List\Playlist\PlaylistTrack; use App\Contracts\GraphQL\Fields\CreatableField; use App\Contracts\GraphQL\Fields\RequiredOnCreation; use App\Contracts\GraphQL\Fields\UpdatableField; +use App\GraphQL\Resolvers\List\Playlist\PlaylistTrackResolver; use App\GraphQL\Schema\Fields\Field; use App\Models\List\Playlist\PlaylistTrack; use App\Models\Wiki\Video; @@ -42,7 +43,7 @@ class PlaylistTrackVideoIdField extends Field implements CreatableField, Require 'integer', Rule::exists(Video::class, Video::ATTRIBUTE_ID), Rule::exists(AnimeThemeEntryVideo::class, AnimeThemeEntryVideo::ATTRIBUTE_VIDEO) - ->where(AnimeThemeEntryVideo::ATTRIBUTE_ENTRY, Arr::get($args, PlaylistTrack::ATTRIBUTE_ENTRY)), + ->where(AnimeThemeEntryVideo::ATTRIBUTE_ENTRY, Arr::get($args, PlaylistTrackResolver::ATTRIBUTE_ENTRY)), ]; } @@ -51,7 +52,7 @@ class PlaylistTrackVideoIdField extends Field implements CreatableField, Require */ public function getUpdateRules(array $args): array { - $entryId = Arr::get($args, PlaylistTrack::ATTRIBUTE_ENTRY); + $entryId = Arr::get($args, PlaylistTrackResolver::ATTRIBUTE_ENTRY); return [ 'sometimes', diff --git a/app/GraphQL/Schema/Fields/Relations/Relation.php b/app/GraphQL/Schema/Fields/Relations/Relation.php index a6348eef3..e5a61f2ed 100644 --- a/app/GraphQL/Schema/Fields/Relations/Relation.php +++ b/app/GraphQL/Schema/Fields/Relations/Relation.php @@ -89,7 +89,7 @@ abstract class Relation extends Field $type = $this->relatedType; - if ($this->paginationType() !== PaginationType::NONE && $type instanceof BaseType) { + if ($this->paginationType() !== PaginationType::NONE && $type instanceof BaseType && $type->hasFilterableColumns()) { $arguments[] = FilterCriteria::getFilters($type) ->map(fn (Filter $filter): array => $filter->getArguments()) ->flatten(); @@ -99,7 +99,7 @@ abstract class Relation extends Field $arguments[] = new FirstArgument(true); $arguments[] = new PageArgument(); - if ($type instanceof BaseType) { + if ($type instanceof BaseType && $type->hasSortableColumns()) { $arguments[] = new SortArgument($type); } } diff --git a/app/GraphQL/Schema/Fields/User/WatchHistory/WatchHistoryEntryIdField.php b/app/GraphQL/Schema/Fields/User/WatchHistory/WatchHistoryEntryIdField.php new file mode 100644 index 000000000..83b4c5920 --- /dev/null +++ b/app/GraphQL/Schema/Fields/User/WatchHistory/WatchHistoryEntryIdField.php @@ -0,0 +1,48 @@ + $args + */ + public function getCreationRules(array $args): array + { + return [ + 'required', + 'integer', + Rule::exists(AnimeThemeEntry::class, AnimeThemeEntry::ATTRIBUTE_ID), + Rule::exists(AnimeThemeEntryVideo::class, AnimeThemeEntryVideo::ATTRIBUTE_ENTRY) + ->where(AnimeThemeEntryVideo::ATTRIBUTE_VIDEO, Arr::get($args, WatchResolver::ATTRIBUTE_VIDEO)), + ]; + } +} diff --git a/app/GraphQL/Schema/Fields/User/WatchHistory/WatchHistoryVideoIdField.php b/app/GraphQL/Schema/Fields/User/WatchHistory/WatchHistoryVideoIdField.php new file mode 100644 index 000000000..99b9dd3d4 --- /dev/null +++ b/app/GraphQL/Schema/Fields/User/WatchHistory/WatchHistoryVideoIdField.php @@ -0,0 +1,48 @@ + $args + */ + public function getCreationRules(array $args): array + { + return [ + 'required', + 'integer', + Rule::exists(Video::class, Video::ATTRIBUTE_ID), + Rule::exists(AnimeThemeEntryVideo::class, AnimeThemeEntryVideo::ATTRIBUTE_VIDEO) + ->where(AnimeThemeEntryVideo::ATTRIBUTE_ENTRY, Arr::get($args, WatchResolver::ATTRIBUTE_ENTRY)), + ]; + } +} diff --git a/app/GraphQL/Schema/Mutations/BaseMutation.php b/app/GraphQL/Schema/Mutations/BaseMutation.php index 0168bfefd..f04ce23bf 100644 --- a/app/GraphQL/Schema/Mutations/BaseMutation.php +++ b/app/GraphQL/Schema/Mutations/BaseMutation.php @@ -61,6 +61,11 @@ abstract class BaseMutation extends Mutation */ abstract public function arguments(): array; + public function type(): Type + { + return $this->toType(); + } + /** * Convert the rebing type to a GraphQL type. */ diff --git a/app/GraphQL/Schema/Mutations/Models/CreateMutation.php b/app/GraphQL/Schema/Mutations/Models/CreateMutation.php index 5ccc5e293..232ef7bed 100644 --- a/app/GraphQL/Schema/Mutations/Models/CreateMutation.php +++ b/app/GraphQL/Schema/Mutations/Models/CreateMutation.php @@ -23,9 +23,11 @@ abstract class CreateMutation extends BaseMutation /** * @param class-string $model */ - public function __construct(protected string $model) - { - parent::__construct('Create'.Str::pascal(class_basename($model))); + public function __construct( + protected string $model, + protected ?string $customName = null, + ) { + parent::__construct($customName ?? 'Create'.Str::pascal(class_basename($model))); } public function authorize($root, array $args, $ctx, ?ResolveInfo $resolveInfo = null, ?Closure $getSelectFields = null): bool @@ -70,7 +72,7 @@ abstract class CreateMutation extends BaseMutation if ($baseType instanceof BaseType) { return collect($baseType->fieldClasses()) ->filter(fn (Field $field): bool => $field instanceof CreatableField) - ->mapWithKeys(fn (Field&CreatableField $field): array => [$field->getColumn() => $field->getCreationRules($args)]) + ->mapWithKeys(fn (Field&CreatableField $field): array => [$field->getName() => $field->getCreationRules($args)]) ->all(); } diff --git a/app/GraphQL/Schema/Mutations/Models/UpdateMutation.php b/app/GraphQL/Schema/Mutations/Models/UpdateMutation.php index f03afe78f..ff32a7c14 100644 --- a/app/GraphQL/Schema/Mutations/Models/UpdateMutation.php +++ b/app/GraphQL/Schema/Mutations/Models/UpdateMutation.php @@ -70,7 +70,7 @@ abstract class UpdateMutation extends BaseMutation if ($baseType instanceof BaseType) { return collect($baseType->fieldClasses()) ->filter(fn (Field $field): bool => $field instanceof UpdatableField) - ->mapWithKeys(fn (Field&UpdatableField $field): array => [$field->getColumn() => $field->getUpdateRules($args)]) + ->mapWithKeys(fn (Field&UpdatableField $field): array => [$field->getName() => $field->getUpdateRules($args)]) ->all(); } diff --git a/app/GraphQL/Schema/Mutations/Models/User/WatchMutation.php b/app/GraphQL/Schema/Mutations/Models/User/WatchMutation.php new file mode 100644 index 000000000..d82a6f160 --- /dev/null +++ b/app/GraphQL/Schema/Mutations/Models/User/WatchMutation.php @@ -0,0 +1,49 @@ +response = Gate::inspect('create', WatchHistory::class))->allowed(); + } + + /** + * The base return type of the query. + */ + public function baseType(): WatchHistoryType + { + return new WatchHistoryType(); + } + + /** + * @param array $args + */ + public function resolve($root, array $args, $context, ResolveInfo $resolveInfo): mixed + { + return App::make(WatchResolver::class) + ->store($root, $args); + } +} diff --git a/app/GraphQL/Schema/Queries/BaseQuery.php b/app/GraphQL/Schema/Queries/BaseQuery.php index 8f89799b0..e2fc190c1 100644 --- a/app/GraphQL/Schema/Queries/BaseQuery.php +++ b/app/GraphQL/Schema/Queries/BaseQuery.php @@ -75,11 +75,11 @@ abstract class BaseQuery extends Query $arguments[] = new PageArgument(); } - if ($baseType instanceof BaseType) { + if ($baseType instanceof BaseType && $baseType->hasFilterableColumns()) { $arguments[] = FilterCriteria::getFilters($baseType)->map(fn (Filter $filter): array => $filter->getArguments())->flatten(); } - if ($baseType instanceof BaseType && $this instanceof EloquentPaginationQuery) { + if ($baseType instanceof BaseType && $this instanceof EloquentPaginationQuery && $baseType->hasSortableColumns()) { $arguments[] = new SortArgument($baseType); } diff --git a/app/GraphQL/Schema/Queries/Models/Pagination/EloquentPaginationQuery.php b/app/GraphQL/Schema/Queries/Models/Pagination/EloquentPaginationQuery.php index 6f9ac8567..1a0d8b34b 100644 --- a/app/GraphQL/Schema/Queries/Models/Pagination/EloquentPaginationQuery.php +++ b/app/GraphQL/Schema/Queries/Models/Pagination/EloquentPaginationQuery.php @@ -5,10 +5,8 @@ declare(strict_types=1); namespace App\GraphQL\Schema\Queries\Models\Pagination; use App\Actions\GraphQL\IndexAction; -use App\GraphQL\Schema\Enums\SortableColumns; use App\GraphQL\Schema\Queries\Models\EloquentQuery; use App\GraphQL\Schema\Types\BaseType; -use App\GraphQL\Schema\Types\EloquentType; use Closure; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; @@ -21,18 +19,9 @@ use RuntimeException; abstract class EloquentPaginationQuery extends EloquentQuery { - protected static bool $typesLoaded = false; - public function __construct(protected string $name) { parent::__construct($name, false, true); - - if (static::$typesLoaded === false) { - static::$typesLoaded = true; - foreach (EloquentType::$typesToLoad as $type) { - GraphQL::addType(new SortableColumns($type)); - } - } } public function authorize($root, array $args, $ctx, ?ResolveInfo $resolveInfo = null, ?Closure $getSelectFields = null): bool diff --git a/app/GraphQL/Schema/Schemas/V1Schema.php b/app/GraphQL/Schema/Schemas/V1Schema.php index f5d00cb61..7876e7de4 100644 --- a/app/GraphQL/Schema/Schemas/V1Schema.php +++ b/app/GraphQL/Schema/Schemas/V1Schema.php @@ -11,6 +11,7 @@ use App\GraphQL\Schema\Mutations\Models\List\Playlist\Track\CreatePlaylistTrackM use App\GraphQL\Schema\Mutations\Models\List\Playlist\Track\DeletePlaylistTrackMutation; use App\GraphQL\Schema\Mutations\Models\List\Playlist\Track\UpdatePlaylistTrackMutation; use App\GraphQL\Schema\Mutations\Models\List\Playlist\UpdatePlaylistMutation; +use App\GraphQL\Schema\Mutations\Models\User\WatchMutation; use App\GraphQL\Schema\Queries\Admin\CurrentFeaturedThemeQuery; use App\GraphQL\Schema\Queries\Auth\MeQuery; use App\GraphQL\Schema\Queries\Models\Pagination\Admin\AnnouncementPaginationQuery; @@ -63,8 +64,15 @@ use App\GraphQL\Schema\Types\List\ExternalProfileType; use App\GraphQL\Schema\Types\List\Playlist\PlaylistTrackType; use App\GraphQL\Schema\Types\List\PlaylistType; use App\GraphQL\Schema\Types\MessageResponseType; +use App\GraphQL\Schema\Types\Pivot\Morph\ImageableType; +use App\GraphQL\Schema\Types\Pivot\Morph\ResourceableType; +use App\GraphQL\Schema\Types\Pivot\Wiki\AnimeSeriesType; +use App\GraphQL\Schema\Types\Pivot\Wiki\AnimeStudioType; +use App\GraphQL\Schema\Types\Pivot\Wiki\AnimeThemeEntryVideoType; +use App\GraphQL\Schema\Types\Pivot\Wiki\ArtistMemberType; use App\GraphQL\Schema\Types\SearchType; use App\GraphQL\Schema\Types\User\Notification\ExternalProfileSyncedNotificationType; +use App\GraphQL\Schema\Types\User\WatchHistoryType; use App\GraphQL\Schema\Types\Wiki\Anime\AnimeSynonymType; use App\GraphQL\Schema\Types\Wiki\Anime\AnimeThemeType; use App\GraphQL\Schema\Types\Wiki\Anime\AnimeYear\AnimeYearSeasonsType; @@ -163,6 +171,8 @@ class V1Schema implements ConfigConvertible CreatePlaylistTrackMutation::class, UpdatePlaylistTrackMutation::class, DeletePlaylistTrackMutation::class, + + WatchMutation::class, ], 'types' => [ @@ -189,6 +199,7 @@ class V1Schema implements ConfigConvertible // User ExternalProfileSyncedNotificationType::class, + WatchHistoryType::class, // Wiki AnimeType::class, @@ -216,10 +227,16 @@ class V1Schema implements ConfigConvertible SearchType::class, // Pivot - // ResourceableType::class, + // These are needed to build related types like + // {Type}SortableColumns and {Type}FilterableColumns. + ImageableType::class, + ResourceableType::class, + AnimeSeriesType::class, + AnimeStudioType::class, + AnimeThemeEntryVideoType::class, + ArtistMemberType::class, // Unions - ImageableUnion::class, LikedUnion::class, NotificationUnion::class, diff --git a/app/GraphQL/Schema/Types/Auth/User/MeType.php b/app/GraphQL/Schema/Types/Auth/User/MeType.php index 469be8f29..74920835e 100644 --- a/app/GraphQL/Schema/Types/Auth/User/MeType.php +++ b/app/GraphQL/Schema/Types/Auth/User/MeType.php @@ -20,6 +20,7 @@ use App\GraphQL\Schema\Types\Auth\PermissionType; use App\GraphQL\Schema\Types\Auth\RoleType; use App\GraphQL\Schema\Types\EloquentType; use App\GraphQL\Schema\Types\List\PlaylistType; +use App\GraphQL\Schema\Types\User\WatchHistoryType; use App\GraphQL\Schema\Types\Wiki\Anime\Theme\AnimeThemeEntryType; use App\GraphQL\Schema\Unions\NotificationUnion; use App\Models\Auth\User; @@ -53,6 +54,7 @@ class MeType extends EloquentType new MorphToManyRelation($this, new PermissionType(), User::RELATION_PERMISSIONS), new BelongsToManyRelation($this, new AnimeThemeEntryType(), 'likedentries'), new BelongsToManyRelation($this, new PlaylistType(), 'likedplaylists'), + new HasManyRelation(new WatchHistoryType(), User::RELATION_WATCH_HISTORY), ]; } @@ -65,4 +67,14 @@ class MeType extends EloquentType { return User::class; } + + public function hasSortableColumns(): bool + { + return false; + } + + public function hasFilterableColumns(): bool + { + return false; + } } diff --git a/app/GraphQL/Schema/Types/Auth/UserType.php b/app/GraphQL/Schema/Types/Auth/UserType.php index 784a46cda..ded15d6b0 100644 --- a/app/GraphQL/Schema/Types/Auth/UserType.php +++ b/app/GraphQL/Schema/Types/Auth/UserType.php @@ -37,4 +37,14 @@ class UserType extends EloquentType new HasManyRelation(new PlaylistType(), User::RELATION_PLAYLISTS), ]; } + + public function hasSortableColumns(): bool + { + return false; + } + + public function hasFilterableColumns(): bool + { + return false; + } } diff --git a/app/GraphQL/Schema/Types/BaseType.php b/app/GraphQL/Schema/Types/BaseType.php index af1f02370..344c1e050 100644 --- a/app/GraphQL/Schema/Types/BaseType.php +++ b/app/GraphQL/Schema/Types/BaseType.php @@ -6,6 +6,8 @@ namespace App\GraphQL\Schema\Types; use App\Contracts\GraphQL\Fields\DeprecatedField; use App\Contracts\GraphQL\Fields\DisplayableField; +use App\Contracts\GraphQL\Fields\FilterableField; +use App\Contracts\GraphQL\Fields\SortableField; use App\GraphQL\Schema\Fields\Field; use App\GraphQL\Schema\Fields\Relations\Relation; use Illuminate\Support\Str; @@ -98,4 +100,14 @@ abstract class BaseType extends RebingType return $fields->merge($relations)->all(); } + + public function hasSortableColumns(): bool + { + return array_any($this->fieldClasses(), fn (Field $field): bool => $field instanceof SortableField); + } + + public function hasFilterableColumns(): bool + { + return array_any($this->fieldClasses(), fn (Field $field): bool => $field instanceof FilterableField); + } } diff --git a/app/GraphQL/Schema/Types/EloquentType.php b/app/GraphQL/Schema/Types/EloquentType.php index c556481bb..912be1ffd 100644 --- a/app/GraphQL/Schema/Types/EloquentType.php +++ b/app/GraphQL/Schema/Types/EloquentType.php @@ -9,15 +9,6 @@ use Illuminate\Support\Str; abstract class EloquentType extends BaseType { - public static array $typesToLoad = []; - - public function __construct() - { - if (! in_array($this, static::$typesToLoad)) { - static::$typesToLoad[] = $this; - } - } - /** * @return array */ diff --git a/app/GraphQL/Schema/Types/User/WatchHistoryType.php b/app/GraphQL/Schema/Types/User/WatchHistoryType.php new file mode 100644 index 000000000..96838aef8 --- /dev/null +++ b/app/GraphQL/Schema/Types/User/WatchHistoryType.php @@ -0,0 +1,39 @@ +nonNullable(), + new BelongsToRelation(new VideoType(), WatchHistory::RELATION_VIDEO) + ->nonNullable(), + ]; + } +} diff --git a/app/Models/Auth/User.php b/app/Models/Auth/User.php index c2447a9b4..d4a2d37f2 100644 --- a/app/Models/Auth/User.php +++ b/app/Models/Auth/User.php @@ -20,6 +20,7 @@ use App\Models\List\Playlist; use App\Models\User\Like; use App\Models\User\Notification; use App\Models\User\Submission; +use App\Models\User\WatchHistory; use App\Models\Wiki\Anime\Theme\AnimeThemeEntry; use Database\Factories\Auth\UserFactory; use Filament\Facades\Filament; @@ -97,6 +98,7 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasSubtit final public const string RELATION_SUBMISSIONS = 'submissions'; final public const string RELATION_ROLES = 'roles'; final public const string RELATION_ROLES_PERMISSIONS = 'roles.permissions'; + final public const string RELATION_WATCH_HISTORY = 'watchHistory'; /** * The table associated with the model. @@ -290,6 +292,14 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasSubtit return $notifications->whereNot(Notification::ATTRIBUTE_TYPE, FilamentNotification::class); } + /** + * @return HasMany + */ + public function watchHistory(): HasMany + { + return $this->hasMany(WatchHistory::class, WatchHistory::ATTRIBUTE_USER); + } + /** * Get the prunable model query. */ diff --git a/app/Models/List/Playlist.php b/app/Models/List/Playlist.php index 064b8f204..0cd7dde7a 100644 --- a/app/Models/List/Playlist.php +++ b/app/Models/List/Playlist.php @@ -198,6 +198,7 @@ class Playlist extends BaseModel implements HasAggregateLikes, HasHashids, HasIm return $this->morphToMany(Image::class, Imageable::RELATION_IMAGEABLE, Imageable::TABLE, Imageable::ATTRIBUTE_IMAGEABLE_ID, Imageable::ATTRIBUTE_IMAGE) ->using(Imageable::class) ->as('playlistimage') + ->withPivot(Imageable::ATTRIBUTE_DEPTH) ->withTimestamps(); } diff --git a/app/Models/User/Like.php b/app/Models/User/Like.php index 26792b0e2..1ed9e0291 100644 --- a/app/Models/User/Like.php +++ b/app/Models/User/Like.php @@ -6,6 +6,8 @@ namespace App\Models\User; use App\Models\Auth\User; use App\Models\BaseModel; +use Database\Factories\User\LikeFactory; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; @@ -16,9 +18,13 @@ use Illuminate\Database\Eloquent\Relations\MorphTo; * @property Model $likeable * @property string $likeable_type * @property int $likeable_id + * + * @method static LikeFactory factory(...$parameters) */ class Like extends BaseModel { + use HasFactory; + final public const string TABLE = 'likes'; final public const string ATTRIBUTE_ID = 'like_id'; diff --git a/app/Models/User/WatchHistory.php b/app/Models/User/WatchHistory.php new file mode 100644 index 000000000..0d104f8c1 --- /dev/null +++ b/app/Models/User/WatchHistory.php @@ -0,0 +1,98 @@ + + */ + protected $fillable = [ + WatchHistory::ATTRIBUTE_ENTRY, + WatchHistory::ATTRIBUTE_USER, + WatchHistory::ATTRIBUTE_VIDEO, + ]; + + public function getName(): string + { + return strval($this->getKey()); + } + + public function getSubtitle(): string + { + return ''; + } + + /** + * @return BelongsTo + */ + public function animethemeentry(): BelongsTo + { + return $this->belongsTo(AnimeThemeEntry::class, WatchHistory::ATTRIBUTE_ENTRY); + } + + /** + * @return BelongsTo + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class, WatchHistory::ATTRIBUTE_USER); + } + + /** + * @return BelongsTo + */ + public function video(): BelongsTo + { + return $this->belongsTo(Video::class, WatchHistory::ATTRIBUTE_VIDEO); + } +} diff --git a/app/Models/Wiki/Anime.php b/app/Models/Wiki/Anime.php index 068586440..19c03835a 100644 --- a/app/Models/Wiki/Anime.php +++ b/app/Models/Wiki/Anime.php @@ -243,6 +243,7 @@ class Anime extends BaseModel implements Auditable, HasImages, HasResources, Sof return $this->morphToMany(Image::class, Imageable::RELATION_IMAGEABLE, Imageable::TABLE, Imageable::ATTRIBUTE_IMAGEABLE_ID, Imageable::ATTRIBUTE_IMAGE) ->using(Imageable::class) ->as('animeimage') + ->withPivot(Imageable::ATTRIBUTE_DEPTH) ->withTimestamps(); } diff --git a/app/Models/Wiki/Studio.php b/app/Models/Wiki/Studio.php index 3f38f749b..08b1559dd 100644 --- a/app/Models/Wiki/Studio.php +++ b/app/Models/Wiki/Studio.php @@ -143,6 +143,7 @@ class Studio extends BaseModel implements Auditable, HasImages, HasResources, So return $this->morphToMany(Image::class, Imageable::RELATION_IMAGEABLE, Imageable::TABLE, Imageable::ATTRIBUTE_IMAGEABLE_ID, Imageable::ATTRIBUTE_IMAGE) ->using(Imageable::class) ->as('studioimage') + ->withPivot(Imageable::ATTRIBUTE_DEPTH) ->withTimestamps(); } } diff --git a/app/Policies/User/LikePolicy.php b/app/Policies/User/LikePolicy.php index b356e41fc..ba5f17688 100644 --- a/app/Policies/User/LikePolicy.php +++ b/app/Policies/User/LikePolicy.php @@ -5,23 +5,28 @@ declare(strict_types=1); namespace App\Policies\User; use App\Enums\Auth\CrudPermission; +use App\Enums\Auth\Role; use App\Models\Auth\User; use App\Models\User\Like; use App\Policies\BasePolicy; +use Filament\Facades\Filament; use Illuminate\Auth\Access\Response; +use Illuminate\Database\Eloquent\Model; class LikePolicy extends BasePolicy { - public function create(User $user): Response + /** + * @param Like $like + */ + public function delete(User $user, Model $like): Response { - return $user->can(CrudPermission::DELETE->format(Like::class)) - ? Response::allow() - : Response::deny(); - } + if (Filament::isServing()) { + return $user->hasRole(Role::ADMIN->value) + ? Response::allow() + : Response::deny(); + } - public function delete(User $user, mixed $value): Response - { - return $user->can(CrudPermission::DELETE->format(Like::class)) + return $like->user()->is($user) && $user->can(CrudPermission::DELETE->format(static::getModel())) ? Response::allow() : Response::deny(); } diff --git a/app/Policies/User/WatchHistoryPolicy.php b/app/Policies/User/WatchHistoryPolicy.php new file mode 100644 index 000000000..d57d53c36 --- /dev/null +++ b/app/Policies/User/WatchHistoryPolicy.php @@ -0,0 +1,33 @@ +hasRole(Role::ADMIN->value) + ? Response::allow() + : Response::deny(); + } + + return $watchHistory->user()->is($user) && $user->can(CrudPermission::DELETE->format(static::getModel())) + ? Response::allow() + : Response::deny(); + } +} diff --git a/database/factories/User/LikeFactory.php b/database/factories/User/LikeFactory.php new file mode 100644 index 000000000..055c77cf4 --- /dev/null +++ b/database/factories/User/LikeFactory.php @@ -0,0 +1,49 @@ + + */ +class LikeFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var class-string + */ + protected $model = Like::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + Like::ATTRIBUTE_USER => User::factory(), + ]; + } + + public function forPlaylist(): static + { + return $this->for(Playlist::factory(), Like::RELATION_LIKEABLE); + } + + public function forEntry(): static + { + return $this->for(AnimeThemeEntry::factory(), Like::RELATION_LIKEABLE); + } +} diff --git a/database/factories/User/WatchHistoryFactory.php b/database/factories/User/WatchHistoryFactory.php new file mode 100644 index 000000000..d92210240 --- /dev/null +++ b/database/factories/User/WatchHistoryFactory.php @@ -0,0 +1,41 @@ + + */ +class WatchHistoryFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var class-string + */ + protected $model = WatchHistory::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + WatchHistory::ATTRIBUTE_ENTRY => AnimeThemeEntry::factory(), + WatchHistory::ATTRIBUTE_USER => User::factory(), + WatchHistory::ATTRIBUTE_VIDEO => Video::factory(), + ]; + } +} diff --git a/database/migrations/2026_02_13_034452_create_watch_history_table.php b/database/migrations/2026_02_13_034452_create_watch_history_table.php new file mode 100644 index 000000000..c5b596113 --- /dev/null +++ b/database/migrations/2026_02_13_034452_create_watch_history_table.php @@ -0,0 +1,33 @@ +id('id'); + + $table->unsignedBigInteger('entry_id'); + $table->foreign('entry_id')->references('entry_id')->on('anime_theme_entries')->cascadeOnDelete(); + + $table->unsignedBigInteger('user_id'); + $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete(); + + $table->unsignedBigInteger('video_id'); + $table->foreign('video_id')->references('video_id')->on('videos')->cascadeOnDelete(); + + $table->timestamps(6); + }); + } + } +}; diff --git a/database/seeders/Auth/Permission/PermissionSeeder.php b/database/seeders/Auth/Permission/PermissionSeeder.php index 6a8f22c5d..d5a262b11 100644 --- a/database/seeders/Auth/Permission/PermissionSeeder.php +++ b/database/seeders/Auth/Permission/PermissionSeeder.php @@ -25,6 +25,7 @@ use App\Models\List\Playlist\PlaylistTrack; use App\Models\User\Like; use App\Models\User\Notification; use App\Models\User\Submission; +use App\Models\User\WatchHistory; use App\Models\Wiki\Anime; use App\Models\Wiki\Anime\AnimeSynonym; use App\Models\Wiki\Anime\AnimeTheme; @@ -78,9 +79,10 @@ class PermissionSeeder extends Seeder $this->registerResource(PlaylistTrack::class, CrudPermission::cases()); // User Resources - $this->registerResource(Like::class, [CrudPermission::VIEW, CrudPermission::CREATE, CrudPermission::DELETE]); - $this->registerResource(Notification::class, [CrudPermission::VIEW, CrudPermission::UPDATE]); + $this->registerResource(Like::class, CrudPermission::cases()); + $this->registerResource(Notification::class, CrudPermission::cases()); $this->registerResource(Submission::class, CrudPermission::cases()); + $this->registerResource(WatchHistory::class, CrudPermission::cases()); // Wiki Resources $this->registerResource(Anime::class, $extendedCrudPermissions); diff --git a/database/seeders/Auth/Role/AdminSeeder.php b/database/seeders/Auth/Role/AdminSeeder.php index 859bfc253..859eb3976 100644 --- a/database/seeders/Auth/Role/AdminSeeder.php +++ b/database/seeders/Auth/Role/AdminSeeder.php @@ -26,6 +26,7 @@ use App\Models\List\Playlist\PlaylistTrack; use App\Models\User\Like; use App\Models\User\Notification; use App\Models\User\Submission; +use App\Models\User\WatchHistory; use App\Models\Wiki\Anime; use App\Models\Wiki\Anime\AnimeSynonym; use App\Models\Wiki\Anime\AnimeTheme; @@ -86,9 +87,10 @@ class AdminSeeder extends RoleSeeder $this->configureResource($role, PlaylistTrack::class, CrudPermission::cases()); // User Resources - $this->configureResource($role, Like::class, [CrudPermission::VIEW, CrudPermission::CREATE, CrudPermission::DELETE]); - $this->configureResource($role, Notification::class, [CrudPermission::VIEW, CrudPermission::UPDATE]); + $this->configureResource($role, Like::class, CrudPermission::cases()); + $this->configureResource($role, Notification::class, CrudPermission::cases()); $this->configureResource($role, Submission::class, CrudPermission::cases()); + $this->configureResource($role, WatchHistory::class, CrudPermission::cases()); // Wiki Resources $this->configureResource($role, Anime::class, $extendedCrudPermissions); diff --git a/database/seeders/Auth/Role/ContentModeratorRoleSeeder.php b/database/seeders/Auth/Role/ContentModeratorRoleSeeder.php index 3e92d003b..b0ea46dda 100644 --- a/database/seeders/Auth/Role/ContentModeratorRoleSeeder.php +++ b/database/seeders/Auth/Role/ContentModeratorRoleSeeder.php @@ -17,6 +17,7 @@ use App\Models\List\Playlist; use App\Models\List\Playlist\PlaylistTrack; use App\Models\User\Like; use App\Models\User\Notification; +use App\Models\User\WatchHistory; use App\Models\Wiki\Anime; use App\Models\Wiki\Anime\AnimeSynonym; use App\Models\Wiki\Anime\AnimeTheme; @@ -63,8 +64,9 @@ class ContentModeratorRoleSeeder extends RoleSeeder $this->configureResource($role, PlaylistTrack::class, CrudPermission::cases()); // User Resources - $this->configureResource($role, Like::class, [CrudPermission::VIEW, CrudPermission::CREATE, CrudPermission::DELETE]); - $this->configureResource($role, Notification::class, [CrudPermission::VIEW, CrudPermission::UPDATE]); + $this->configureResource($role, Like::class, CrudPermission::cases()); + $this->configureResource($role, Notification::class, CrudPermission::cases()); + $this->configureResource($role, WatchHistory::class, CrudPermission::cases()); $extendedCrudPermissions = array_merge( CrudPermission::cases(), diff --git a/database/seeders/Auth/Role/ContributorRoleSeeder.php b/database/seeders/Auth/Role/ContributorRoleSeeder.php index c37de99cd..f05952aa9 100644 --- a/database/seeders/Auth/Role/ContributorRoleSeeder.php +++ b/database/seeders/Auth/Role/ContributorRoleSeeder.php @@ -16,6 +16,7 @@ use App\Models\List\Playlist; use App\Models\List\Playlist\PlaylistTrack; use App\Models\User\Like; use App\Models\User\Notification; +use App\Models\User\WatchHistory; use App\Models\Wiki\Anime; use App\Models\Wiki\Anime\AnimeSynonym; use App\Models\Wiki\Anime\AnimeTheme; @@ -57,8 +58,9 @@ class ContributorRoleSeeder extends RoleSeeder $this->configureResource($role, PlaylistTrack::class, CrudPermission::cases()); // User Resources - $this->configureResource($role, Like::class, [CrudPermission::VIEW, CrudPermission::CREATE, CrudPermission::DELETE]); - $this->configureResource($role, Notification::class, [CrudPermission::VIEW, CrudPermission::UPDATE]); + $this->configureResource($role, Like::class, CrudPermission::cases()); + $this->configureResource($role, Notification::class, CrudPermission::cases()); + $this->configureResource($role, WatchHistory::class, CrudPermission::cases()); // Wiki Resources $this->configureResource($role, Anime::class, [CrudPermission::VIEW]); diff --git a/database/seeders/Auth/Role/DeveloperRoleSeeder.php b/database/seeders/Auth/Role/DeveloperRoleSeeder.php index c7d819152..787fc1cb8 100644 --- a/database/seeders/Auth/Role/DeveloperRoleSeeder.php +++ b/database/seeders/Auth/Role/DeveloperRoleSeeder.php @@ -16,6 +16,7 @@ use App\Models\List\Playlist; use App\Models\List\Playlist\PlaylistTrack; use App\Models\User\Like; use App\Models\User\Notification; +use App\Models\User\WatchHistory; use App\Models\Wiki\Anime; use App\Models\Wiki\Anime\AnimeSynonym; use App\Models\Wiki\Anime\AnimeTheme; @@ -57,8 +58,9 @@ class DeveloperRoleSeeder extends RoleSeeder $this->configureResource($role, PlaylistTrack::class, CrudPermission::cases()); // User Resources - $this->configureResource($role, Like::class, [CrudPermission::VIEW, CrudPermission::CREATE, CrudPermission::DELETE]); - $this->configureResource($role, Notification::class, [CrudPermission::VIEW, CrudPermission::UPDATE]); + $this->configureResource($role, Like::class, CrudPermission::cases()); + $this->configureResource($role, Notification::class, CrudPermission::cases()); + $this->configureResource($role, WatchHistory::class, CrudPermission::cases()); // Wiki Resources $this->configureResource($role, Anime::class, [CrudPermission::VIEW]); diff --git a/database/seeders/Auth/Role/EncoderRoleSeeder.php b/database/seeders/Auth/Role/EncoderRoleSeeder.php index 95a230a02..612197648 100644 --- a/database/seeders/Auth/Role/EncoderRoleSeeder.php +++ b/database/seeders/Auth/Role/EncoderRoleSeeder.php @@ -15,7 +15,9 @@ use App\Models\List\External\ExternalEntry; use App\Models\List\ExternalProfile; use App\Models\List\Playlist; use App\Models\List\Playlist\PlaylistTrack; +use App\Models\User\Like; use App\Models\User\Notification; +use App\Models\User\WatchHistory; use App\Models\Wiki\Anime; use App\Models\Wiki\Anime\AnimeSynonym; use App\Models\Wiki\Anime\AnimeTheme; @@ -69,7 +71,9 @@ class EncoderRoleSeeder extends RoleSeeder ); // User Resources - $this->configureResource($role, Notification::class, [CrudPermission::VIEW, CrudPermission::UPDATE]); + $this->configureResource($role, Like::class, CrudPermission::cases()); + $this->configureResource($role, Notification::class, CrudPermission::cases()); + $this->configureResource($role, WatchHistory::class, CrudPermission::cases()); // Wiki Resources $this->configureResource($role, Anime::class, $extendedCrudPermissions); diff --git a/database/seeders/Auth/Role/PanelViewerRoleSeeder.php b/database/seeders/Auth/Role/PanelViewerRoleSeeder.php index 9864a9fd3..8372374c9 100644 --- a/database/seeders/Auth/Role/PanelViewerRoleSeeder.php +++ b/database/seeders/Auth/Role/PanelViewerRoleSeeder.php @@ -16,6 +16,7 @@ use App\Models\List\Playlist; use App\Models\List\Playlist\PlaylistTrack; use App\Models\User\Like; use App\Models\User\Notification; +use App\Models\User\WatchHistory; use App\Models\Wiki\Anime; use App\Models\Wiki\Anime\AnimeSynonym; use App\Models\Wiki\Anime\AnimeTheme; @@ -57,8 +58,9 @@ class PanelViewerRoleSeeder extends RoleSeeder $this->configureResource($role, PlaylistTrack::class, CrudPermission::cases()); // User Resources - $this->configureResource($role, Like::class, [CrudPermission::VIEW, CrudPermission::CREATE, CrudPermission::DELETE]); - $this->configureResource($role, Notification::class, [CrudPermission::VIEW, CrudPermission::UPDATE]); + $this->configureResource($role, Like::class, CrudPermission::cases()); + $this->configureResource($role, Notification::class, CrudPermission::cases()); + $this->configureResource($role, WatchHistory::class, CrudPermission::cases()); // Wiki Resources $this->configureResource($role, Anime::class, [CrudPermission::VIEW]); diff --git a/database/seeders/Auth/Role/PatronRoleSeeder.php b/database/seeders/Auth/Role/PatronRoleSeeder.php index 7d438a14c..44c8e4ca4 100644 --- a/database/seeders/Auth/Role/PatronRoleSeeder.php +++ b/database/seeders/Auth/Role/PatronRoleSeeder.php @@ -16,6 +16,7 @@ use App\Models\List\Playlist; use App\Models\List\Playlist\PlaylistTrack; use App\Models\User\Like; use App\Models\User\Notification; +use App\Models\User\WatchHistory; use App\Models\Wiki\Anime; use App\Models\Wiki\Anime\AnimeSynonym; use App\Models\Wiki\Anime\AnimeTheme; @@ -57,8 +58,9 @@ class PatronRoleSeeder extends RoleSeeder $this->configureResource($role, PlaylistTrack::class, CrudPermission::cases()); // User Resources - $this->configureResource($role, Like::class, [CrudPermission::VIEW, CrudPermission::CREATE, CrudPermission::DELETE]); - $this->configureResource($role, Notification::class, [CrudPermission::VIEW, CrudPermission::UPDATE]); + $this->configureResource($role, Like::class, CrudPermission::cases()); + $this->configureResource($role, Notification::class, CrudPermission::cases()); + $this->configureResource($role, WatchHistory::class, CrudPermission::cases()); // Wiki Resources $this->configureResource($role, Anime::class, [CrudPermission::VIEW]); diff --git a/database/seeders/Auth/Role/VerifiedRoleSeeder.php b/database/seeders/Auth/Role/VerifiedRoleSeeder.php index b37d3aa2e..809e5cecc 100644 --- a/database/seeders/Auth/Role/VerifiedRoleSeeder.php +++ b/database/seeders/Auth/Role/VerifiedRoleSeeder.php @@ -12,6 +12,8 @@ use App\Models\List\ExternalProfile; use App\Models\List\Playlist; use App\Models\List\Playlist\PlaylistTrack; use App\Models\User\Like; +use App\Models\User\Notification; +use App\Models\User\WatchHistory; class VerifiedRoleSeeder extends RoleSeeder { @@ -28,7 +30,9 @@ class VerifiedRoleSeeder extends RoleSeeder $role = Role::findOrCreate($roleEnum->value); // User Resources - $this->configureResource($role, Like::class, [CrudPermission::VIEW, CrudPermission::CREATE, CrudPermission::DELETE]); + $this->configureResource($role, Like::class, CrudPermission::cases()); + $this->configureResource($role, Notification::class, CrudPermission::cases()); + $this->configureResource($role, WatchHistory::class, CrudPermission::cases()); // List Resources $this->configureResource($role, ExternalEntry::class, [CrudPermission::VIEW]); diff --git a/tests/Unit/Models/User/LikeTest.php b/tests/Unit/Models/User/LikeTest.php new file mode 100644 index 000000000..e84807913 --- /dev/null +++ b/tests/Unit/Models/User/LikeTest.php @@ -0,0 +1,55 @@ +forPlaylist() + ->createOne(); + + $this->assertIsString($like->getName()); +}); + +test('has subtitle', function () { + $like = Like::factory() + ->forPlaylist() + ->createOne(); + + $this->assertIsString($like->getSubtitle()); +}); + +test('playlist', function () { + $like = Like::factory() + ->forPlaylist() + ->createOne(); + + $this->assertInstanceOf(MorphTo::class, $like->likeable()); + $this->assertInstanceOf(Playlist::class, $like->likeable()->first()); +}); + +test('entry', function () { + $like = Like::factory() + ->forEntry() + ->createOne(); + + $this->assertInstanceOf(MorphTo::class, $like->likeable()); + $this->assertInstanceOf(AnimeThemeEntry::class, $like->likeable()->first()); +}); + +test('user', function () { + $like = Like::factory() + ->forPlaylist() + ->createOne(); + + $this->assertInstanceOf(BelongsTo::class, $like->user()); + $this->assertInstanceOf(User::class, $like->user()->first()); +}); diff --git a/tests/Unit/Models/User/WatchHistoryTest.php b/tests/Unit/Models/User/WatchHistoryTest.php new file mode 100644 index 000000000..2f99054a6 --- /dev/null +++ b/tests/Unit/Models/User/WatchHistoryTest.php @@ -0,0 +1,44 @@ +createOne(); + + $this->assertIsString($history->getName()); +}); + +test('has subtitle', function () { + $history = WatchHistory::factory()->createOne(); + + $this->assertIsString($history->getSubtitle()); +}); + +test('entry', function () { + $history = WatchHistory::factory()->createOne(); + + $this->assertInstanceOf(BelongsTo::class, $history->animethemeentry()); + $this->assertInstanceOf(AnimeThemeEntry::class, $history->animethemeentry()->first()); +}); + +test('user', function () { + $history = WatchHistory::factory()->createOne(); + + $this->assertInstanceOf(BelongsTo::class, $history->user()); + $this->assertInstanceOf(User::class, $history->user()->first()); +}); + +test('video', function () { + $history = WatchHistory::factory()->createOne(); + + $this->assertInstanceOf(BelongsTo::class, $history->video()); + $this->assertInstanceOf(Video::class, $history->video()->first()); +});