From 40c334ccb3ae807ac6cec4bf07ec31dac139e6f4 Mon Sep 17 00:00:00 2001 From: Kyrch Date: Wed, 27 Aug 2025 03:47:27 -0300 Subject: [PATCH] feat(graphql): notification union (#934) --- app/Enums/Models/User/NotificationType.php | 13 ++++- .../ExternalProfile/ExternalProfileSynced.php | 11 ++-- .../ExternalProfileSyncedProfileIdField.php | 33 ++++++++++++ .../ExternalProfileSyncedProfileNameField.php | 33 ++++++++++++ .../NotificationDataBodyField.php | 23 -------- .../NotificationDataImageField.php | 23 -------- .../NotificationDataTitleField.php | 23 -------- .../Notification/NotificationDataField.php | 34 ------------ .../Notification/NotificationTypeField.php | 34 ++++++++++++ .../Definition/Types/Auth/User/MeType.php | 4 +- ...ExternalProfileSyncedNotificationType.php} | 24 +++++---- .../Notification/NotificationDataType.php | 36 ------------- .../Definition/Unions/ImageableUnion.php | 3 +- app/GraphQL/Definition/Unions/LikedUnion.php | 3 +- .../Definition/Unions/NotificationUnion.php | 54 +++++++++++++++++++ .../Definition/Unions/ResourceableUnion.php | 3 +- .../Support/Relations/MorphManyRelation.php | 2 +- app/GraphQL/Support/Relations/Relation.php | 7 ++- app/Models/Auth/User.php | 3 +- app/Models/User/Notification.php | 20 +++++++ ... => ExternalProfileSyncedNotification.php} | 15 ++---- app/Providers/GraphQLServiceProvider.php | 2 + config/graphql.php | 11 ++-- .../factories/User/NotificationFactory.php | 11 +--- 24 files changed, 235 insertions(+), 190 deletions(-) create mode 100644 app/GraphQL/Definition/Fields/User/Notification/ExternalProfileSynced/ExternalProfileSyncedProfileIdField.php create mode 100644 app/GraphQL/Definition/Fields/User/Notification/ExternalProfileSynced/ExternalProfileSyncedProfileNameField.php delete mode 100644 app/GraphQL/Definition/Fields/User/Notification/NotificationData/NotificationDataBodyField.php delete mode 100644 app/GraphQL/Definition/Fields/User/Notification/NotificationData/NotificationDataImageField.php delete mode 100644 app/GraphQL/Definition/Fields/User/Notification/NotificationData/NotificationDataTitleField.php delete mode 100644 app/GraphQL/Definition/Fields/User/Notification/NotificationDataField.php create mode 100644 app/GraphQL/Definition/Fields/User/Notification/NotificationTypeField.php rename app/GraphQL/Definition/Types/User/{NotificationType.php => Notification/ExternalProfileSyncedNotificationType.php} (50%) delete mode 100644 app/GraphQL/Definition/Types/User/Notification/NotificationDataType.php create mode 100644 app/GraphQL/Definition/Unions/NotificationUnion.php rename app/Notifications/{UserNotification.php => ExternalProfileSyncedNotification.php} (63%) diff --git a/app/Enums/Models/User/NotificationType.php b/app/Enums/Models/User/NotificationType.php index 107e6b1a4..e54d32742 100644 --- a/app/Enums/Models/User/NotificationType.php +++ b/app/Enums/Models/User/NotificationType.php @@ -4,7 +4,18 @@ declare(strict_types=1); namespace App\Enums\Models\User; +use App\Models\User\Notification; +use App\Notifications\ExternalProfileSyncedNotification; + enum NotificationType: int { - case SYNCED_PROFILE = 0; + case PROFILE_SYNCED = 0; + + public static function resolveType(Notification $notification) + { + return match ($notification->getAttribute(Notification::ATTRIBUTE_TYPE)) { + ExternalProfileSyncedNotification::class => self::PROFILE_SYNCED, + default => null, + }; + } } diff --git a/app/Events/List/ExternalProfile/ExternalProfileSynced.php b/app/Events/List/ExternalProfile/ExternalProfileSynced.php index 3227918e4..6f3117c86 100644 --- a/app/Events/List/ExternalProfile/ExternalProfileSynced.php +++ b/app/Events/List/ExternalProfile/ExternalProfileSynced.php @@ -5,9 +5,8 @@ declare(strict_types=1); namespace App\Events\List\ExternalProfile; use App\Contracts\Events\NotifiesUsersEvent; -use App\Enums\Models\User\NotificationType; use App\Models\List\ExternalProfile; -use App\Notifications\UserNotification; +use App\Notifications\ExternalProfileSyncedNotification; use Illuminate\Foundation\Events\Dispatchable; class ExternalProfileSynced implements NotifiesUsersEvent @@ -23,12 +22,8 @@ class ExternalProfileSynced implements NotifiesUsersEvent { $profile = $this->profile; - $notification = new UserNotification( - 'External Profile Synced', - "Your external profile [{$profile->getName()}]({$profile->getClientUrl()}) has been synced.", - NotificationType::SYNCED_PROFILE, + $profile->user?->notifyNow( + new ExternalProfileSyncedNotification($profile) ); - - $profile->user?->notifyNow($notification); } } diff --git a/app/GraphQL/Definition/Fields/User/Notification/ExternalProfileSynced/ExternalProfileSyncedProfileIdField.php b/app/GraphQL/Definition/Fields/User/Notification/ExternalProfileSynced/ExternalProfileSyncedProfileIdField.php new file mode 100644 index 000000000..271ecb25a --- /dev/null +++ b/app/GraphQL/Definition/Fields/User/Notification/ExternalProfileSynced/ExternalProfileSyncedProfileIdField.php @@ -0,0 +1,33 @@ +name; + } +} diff --git a/app/GraphQL/Definition/Types/Auth/User/MeType.php b/app/GraphQL/Definition/Types/Auth/User/MeType.php index 64a4e5e8b..1b13a5a27 100644 --- a/app/GraphQL/Definition/Types/Auth/User/MeType.php +++ b/app/GraphQL/Definition/Types/Auth/User/MeType.php @@ -16,8 +16,8 @@ use App\GraphQL\Definition\Types\Auth\PermissionType; use App\GraphQL\Definition\Types\Auth\RoleType; use App\GraphQL\Definition\Types\EloquentType; use App\GraphQL\Definition\Types\List\PlaylistType; -use App\GraphQL\Definition\Types\User\NotificationType; use App\GraphQL\Definition\Types\Wiki\VideoType; +use App\GraphQL\Definition\Unions\NotificationUnion; use App\GraphQL\Support\Relations\BelongsToManyRelation; use App\GraphQL\Support\Relations\HasManyRelation; use App\GraphQL\Support\Relations\MorphManyRelation; @@ -42,7 +42,7 @@ class MeType extends EloquentType public function relations(): array { return [ - new MorphManyRelation(new NotificationType(), User::RELATION_NOTIFICATIONS), + new MorphManyRelation(new NotificationUnion(), User::RELATION_NOTIFICATIONS), new HasManyRelation(new PlaylistType(), User::RELATION_PLAYLISTS), new BelongsToManyRelation($this, RoleType::class, User::RELATION_ROLES), new BelongsToManyRelation($this, PermissionType::class, User::RELATION_PERMISSIONS), diff --git a/app/GraphQL/Definition/Types/User/NotificationType.php b/app/GraphQL/Definition/Types/User/Notification/ExternalProfileSyncedNotificationType.php similarity index 50% rename from app/GraphQL/Definition/Types/User/NotificationType.php rename to app/GraphQL/Definition/Types/User/Notification/ExternalProfileSyncedNotificationType.php index ba2858be9..45a9ee851 100644 --- a/app/GraphQL/Definition/Types/User/NotificationType.php +++ b/app/GraphQL/Definition/Types/User/Notification/ExternalProfileSyncedNotificationType.php @@ -2,25 +2,28 @@ declare(strict_types=1); -namespace App\GraphQL\Definition\Types\User; +namespace App\GraphQL\Definition\Types\User\Notification; +use App\GraphQL\Definition\Fields\Base\CreatedAtField; use App\GraphQL\Definition\Fields\Field; -use App\GraphQL\Definition\Fields\User\Notification\NotificationDataField; +use App\GraphQL\Definition\Fields\User\Notification\ExternalProfileSynced\ExternalProfileSyncedProfileIdField; +use App\GraphQL\Definition\Fields\User\Notification\ExternalProfileSynced\ExternalProfileSyncedProfileNameField; use App\GraphQL\Definition\Fields\User\Notification\NotificationReadAtField; -use App\GraphQL\Definition\Types\Auth\UserType; +use App\GraphQL\Definition\Fields\User\Notification\NotificationTypeField; use App\GraphQL\Definition\Types\EloquentType; -use App\GraphQL\Support\Relations\MorphToRelation; +use App\GraphQL\Definition\Types\List\ExternalProfileType; +use App\GraphQL\Support\Relations\BelongsToRelation; use App\GraphQL\Support\Relations\Relation; use App\Models\User\Notification; -class NotificationType extends EloquentType +class ExternalProfileSyncedNotificationType extends EloquentType { /** * The description of the type. */ public function description(): string { - return 'Represents a notification that is sent to the user.'; + return 'Represents a notification that is sent to the user when a profile is synced.'; } /** @@ -31,8 +34,8 @@ class NotificationType extends EloquentType public function relations(): array { return [ - new MorphToRelation(new UserType(), Notification::RELATION_NOTIFIABLE) - ->renameTo('user'), + new BelongsToRelation(new ExternalProfileType(), Notification::RELATION_PROFILE) + ->notNullable(), ]; } @@ -44,8 +47,11 @@ class NotificationType extends EloquentType public function fieldClasses(): array { return [ + new NotificationTypeField(), + new ExternalProfileSyncedProfileIdField(), + new ExternalProfileSyncedProfileNameField(), new NotificationReadAtField(), - new NotificationDataField(), + new CreatedAtField(), ]; } } diff --git a/app/GraphQL/Definition/Types/User/Notification/NotificationDataType.php b/app/GraphQL/Definition/Types/User/Notification/NotificationDataType.php deleted file mode 100644 index e086e6e38..000000000 --- a/app/GraphQL/Definition/Types/User/Notification/NotificationDataType.php +++ /dev/null @@ -1,36 +0,0 @@ -type) { + ExternalProfileSyncedNotification::class => new ExternalProfileSyncedNotificationType(), + default => throw new RuntimeException("Type not specified for notification {$value->type}"), + }; + + return GraphQL::type($type->getName()); + } +} diff --git a/app/GraphQL/Definition/Unions/ResourceableUnion.php b/app/GraphQL/Definition/Unions/ResourceableUnion.php index 70cbb6ded..5d20bc96e 100644 --- a/app/GraphQL/Definition/Unions/ResourceableUnion.php +++ b/app/GraphQL/Definition/Unions/ResourceableUnion.php @@ -14,8 +14,9 @@ class ResourceableUnion extends BaseUnion { /** * The name of the union type. + * By default, it will be the class name. */ - public function name(): string + public function getName(): string { return 'Resourceable'; } diff --git a/app/GraphQL/Support/Relations/MorphManyRelation.php b/app/GraphQL/Support/Relations/MorphManyRelation.php index ac86d270b..81f4ffb61 100644 --- a/app/GraphQL/Support/Relations/MorphManyRelation.php +++ b/app/GraphQL/Support/Relations/MorphManyRelation.php @@ -23,6 +23,6 @@ class MorphManyRelation extends Relation */ public function paginationType(): PaginationType { - return PaginationType::CONNECTION; + return PaginationType::PAGINATOR; } } diff --git a/app/GraphQL/Support/Relations/Relation.php b/app/GraphQL/Support/Relations/Relation.php index d29263d86..2f5a00ca2 100644 --- a/app/GraphQL/Support/Relations/Relation.php +++ b/app/GraphQL/Support/Relations/Relation.php @@ -85,14 +85,17 @@ abstract class Relation $type = $this->rebingType; - if ($this->paginationType() !== PaginationType::NONE) { + if ($this->paginationType() !== PaginationType::NONE && $type instanceof BaseType) { $arguments[] = $this->resolveFilterArguments($type->fieldClasses()); } if ($this->paginationType() !== PaginationType::NONE) { $arguments[] = new FirstArgument(true); $arguments[] = new PageArgument(); - $arguments[] = $this->resolveSortArguments($this->rebingType); + + if ($type instanceof BaseType) { + $arguments[] = $this->resolveSortArguments($this->rebingType); + } } return Arr::flatten($arguments); diff --git a/app/Models/Auth/User.php b/app/Models/Auth/User.php index 9fc3609ea..cd45bcc7a 100644 --- a/app/Models/Auth/User.php +++ b/app/Models/Auth/User.php @@ -21,7 +21,6 @@ use App\Models\User\Like; use App\Models\User\Notification; use App\Models\User\Report; use App\Models\Wiki\Video; -use App\Notifications\UserNotification; use Database\Factories\Auth\UserFactory; use Filament\Facades\Filament; use Filament\Models\Contracts\FilamentUser; @@ -298,7 +297,7 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasSubtit return $notifications->where(Notification::ATTRIBUTE_TYPE, FilamentNotification::class); } - return $notifications->where(Notification::ATTRIBUTE_TYPE, UserNotification::class); + return $notifications; } /** diff --git a/app/Models/User/Notification.php b/app/Models/User/Notification.php index 971498a1e..6081f85e4 100644 --- a/app/Models/User/Notification.php +++ b/app/Models/User/Notification.php @@ -4,9 +4,12 @@ declare(strict_types=1); namespace App\Models\User; +use App\Models\List\ExternalProfile; use Database\Factories\User\NotificationFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Notifications\DatabaseNotification; +use Illuminate\Support\Arr; /** * Class Notification. @@ -26,5 +29,22 @@ class Notification extends DatabaseNotification final public const ATTRIBUTE_DATA = 'data'; final public const ATTRIBUTE_READ_AT = 'read_at'; + final public const RELATION_PROFILE = 'profile'; final public const RELATION_NOTIFIABLE = 'notifiable'; + + /** + * Virtual attribute to use in relations. + */ + public function getProfileIdAttribute(): ?int + { + return Arr::get($this->getAttribute(self::ATTRIBUTE_DATA), 'profileId'); + } + + /** + * Virtual relation to the profile. + */ + public function profile(): BelongsTo + { + return $this->belongsTo(ExternalProfile::class, 'profile_id'); + } } diff --git a/app/Notifications/UserNotification.php b/app/Notifications/ExternalProfileSyncedNotification.php similarity index 63% rename from app/Notifications/UserNotification.php rename to app/Notifications/ExternalProfileSyncedNotification.php index c1448da37..cfa93bd25 100644 --- a/app/Notifications/UserNotification.php +++ b/app/Notifications/ExternalProfileSyncedNotification.php @@ -4,21 +4,18 @@ declare(strict_types=1); namespace App\Notifications; -use App\Enums\Models\User\NotificationType; +use App\Models\List\ExternalProfile; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Notifications\Notification; -class UserNotification extends Notification implements Arrayable, ShouldQueue +class ExternalProfileSyncedNotification extends Notification implements Arrayable, ShouldQueue { use Queueable; public function __construct( - public string $title, - public string $body, - public NotificationType $type, - public ?string $image = null, + public ExternalProfile $profile, ) {} /** @@ -41,10 +38,8 @@ class UserNotification extends Notification implements Arrayable, ShouldQueue public function toArray(): array { return [ - 'title' => $this->title, - 'body' => $this->body, - 'type' => $this->type->value, - 'image' => $this->image, + 'profileId' => $this->profile->getKey(), + 'profileName' => $this->profile->getName(), ]; } } diff --git a/app/Providers/GraphQLServiceProvider.php b/app/Providers/GraphQLServiceProvider.php index 408f2ca35..3d7446655 100644 --- a/app/Providers/GraphQLServiceProvider.php +++ b/app/Providers/GraphQLServiceProvider.php @@ -10,6 +10,7 @@ use App\Enums\Models\List\ExternalEntryWatchStatus; use App\Enums\Models\List\ExternalProfileSite; use App\Enums\Models\List\ExternalProfileVisibility; use App\Enums\Models\List\PlaylistVisibility; +use App\Enums\Models\User\NotificationType; use App\Enums\Models\Wiki\AnimeMediaFormat; use App\Enums\Models\Wiki\AnimeSeason; use App\Enums\Models\Wiki\AnimeSynonymType; @@ -58,6 +59,7 @@ class GraphQLServiceProvider extends ServiceProvider GraphQL::addType(new EnumType(ExternalProfileSite::class)); GraphQL::addType(new EnumType(ExternalProfileVisibility::class)); GraphQL::addType(new EnumType(PlaylistVisibility::class)); + GraphQL::addType(new EnumType(NotificationType::class)); GraphQL::addType(new EnumType(AnimeMediaFormat::class)); GraphQL::addType(new EnumType(AnimeSeason::class)); GraphQL::addType(new EnumType(AnimeSynonymType::class)); diff --git a/config/graphql.php b/config/graphql.php index aa7f6c36d..947810522 100644 --- a/config/graphql.php +++ b/config/graphql.php @@ -58,8 +58,7 @@ use App\GraphQL\Definition\Types\List\PlaylistType; use App\GraphQL\Definition\Types\MessageResponseType; use App\GraphQL\Definition\Types\Pivot\Morph\ResourceableType; use App\GraphQL\Definition\Types\SearchType; -use App\GraphQL\Definition\Types\User\Notification\NotificationDataType; -use App\GraphQL\Definition\Types\User\NotificationType; +use App\GraphQL\Definition\Types\User\Notification\ExternalProfileSyncedNotificationType; use App\GraphQL\Definition\Types\Wiki\Anime\AnimeSynonymType; use App\GraphQL\Definition\Types\Wiki\Anime\AnimeThemeType; use App\GraphQL\Definition\Types\Wiki\Anime\AnimeYear\AnimeYearSeasonsType; @@ -79,7 +78,9 @@ use App\GraphQL\Definition\Types\Wiki\StudioType; use App\GraphQL\Definition\Types\Wiki\ThemeGroupType; use App\GraphQL\Definition\Types\Wiki\Video\VideoScriptType; use App\GraphQL\Definition\Types\Wiki\VideoType; +use App\GraphQL\Definition\Unions\ImageableUnion; use App\GraphQL\Definition\Unions\LikedUnion; +use App\GraphQL\Definition\Unions\NotificationUnion; use App\GraphQL\Definition\Unions\PerformanceArtistUnion; use App\GraphQL\Definition\Unions\ResourceableUnion; @@ -226,8 +227,7 @@ return [ PlaylistTrackType::class, // User - NotificationType::class, - NotificationDataType::class, + ExternalProfileSyncedNotificationType::class, // Wiki AnimeType::class, @@ -258,7 +258,10 @@ return [ // ResourceableType::class, // Unions + + ImageableUnion::class, LikedUnion::class, + NotificationUnion::class, PerformanceArtistUnion::class, ResourceableUnion::class, ], diff --git a/database/factories/User/NotificationFactory.php b/database/factories/User/NotificationFactory.php index f791f8635..bc1c80758 100644 --- a/database/factories/User/NotificationFactory.php +++ b/database/factories/User/NotificationFactory.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Database\Factories\User; use App\Models\User\Notification; -use App\Notifications\UserNotification; +use App\Notifications\ExternalProfileSyncedNotification; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; @@ -33,16 +33,9 @@ class NotificationFactory extends Factory */ public function definition(): array { - $data = [ - 'title' => fake()->text(), - 'body' => fake()->text(), - 'image' => fake()->imageUrl(), - ]; - return [ Notification::ATTRIBUTE_ID => Str::uuid()->__toString(), - Notification::ATTRIBUTE_TYPE => UserNotification::class, - Notification::ATTRIBUTE_DATA => $data, + Notification::ATTRIBUTE_TYPE => ExternalProfileSyncedNotification::class, ]; } }