feat(graphql): notification union (#934)

This commit is contained in:
Kyrch
2025-08-27 03:47:27 -03:00
committed by GitHub
parent 7382ed4a37
commit 40c334ccb3
24 changed files with 235 additions and 190 deletions
+12 -1
View File
@@ -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,
};
}
}
@@ -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);
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Definition\Fields\User\Notification\ExternalProfileSynced;
use App\GraphQL\Definition\Fields\IntField;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Support\Arr;
class ExternalProfileSyncedProfileIdField extends IntField
{
public function __construct()
{
parent::__construct('profileId', nullable: false);
}
/**
* The description of the field.
*/
public function description(): string
{
return 'The id of the profile';
}
/**
* Resolve the field.
*/
public function resolve($root, array $args, $context, ResolveInfo $resolveInfo): mixed
{
return Arr::get($root, 'data.profileId');
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Definition\Fields\User\Notification\ExternalProfileSynced;
use App\GraphQL\Definition\Fields\DateTimeTzField;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Support\Arr;
class ExternalProfileSyncedProfileNameField extends DateTimeTzField
{
public function __construct()
{
parent::__construct('profileName', nullable: false);
}
/**
* The description of the field.
*/
public function description(): string
{
return 'The name of the profile';
}
/**
* Resolve the field.
*/
public function resolve($root, array $args, $context, ResolveInfo $resolveInfo): mixed
{
return Arr::get($root, 'data.profileName');
}
}
@@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Definition\Fields\User\Notification\NotificationData;
use App\GraphQL\Definition\Fields\StringField;
class NotificationDataBodyField extends StringField
{
public function __construct()
{
parent::__construct('body', nullable: false);
}
/**
* The description of the field.
*/
public function description(): string
{
return 'The content of the notification';
}
}
@@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Definition\Fields\User\Notification\NotificationData;
use App\GraphQL\Definition\Fields\StringField;
class NotificationDataImageField extends StringField
{
public function __construct()
{
parent::__construct('image');
}
/**
* The description of the field.
*/
public function description(): string
{
return 'The image URL to display with the notification';
}
}
@@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Definition\Fields\User\Notification\NotificationData;
use App\GraphQL\Definition\Fields\StringField;
class NotificationDataTitleField extends StringField
{
public function __construct()
{
parent::__construct('title', nullable: false);
}
/**
* The description of the field.
*/
public function description(): string
{
return 'The title of the notification';
}
}
@@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Definition\Fields\User\Notification;
use App\GraphQL\Definition\Fields\JsonField;
use App\GraphQL\Definition\Types\User\Notification\NotificationDataType;
use App\Models\User\Notification;
use GraphQL\Type\Definition\Type;
class NotificationDataField extends JsonField
{
public function __construct()
{
parent::__construct(Notification::ATTRIBUTE_DATA, nullable: false);
}
/**
* The description of the field.
*/
public function description(): string
{
return 'The JSON data of the notification';
}
/**
* The type returned by the field.
*/
public function baseType(): NotificationDataType
{
return new NotificationDataType();
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Definition\Fields\User\Notification;
use App\Enums\Models\User\NotificationType;
use App\GraphQL\Definition\Fields\EnumField;
use App\Models\User\Notification;
use GraphQL\Type\Definition\ResolveInfo;
class NotificationTypeField extends EnumField
{
public function __construct()
{
parent::__construct(Notification::ATTRIBUTE_TYPE, NotificationType::class, nullable: false);
}
/**
* The description of the field.
*/
public function description(): string
{
return 'The type of the notification';
}
/**
* Resolve the field.
*/
public function resolve($root, array $args, $context, ResolveInfo $resolveInfo): mixed
{
return NotificationType::resolveType($root)->name;
}
}
@@ -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),
@@ -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(),
];
}
}
@@ -1,36 +0,0 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Definition\Types\User\Notification;
use App\GraphQL\Definition\Fields\Field;
use App\GraphQL\Definition\Fields\User\Notification\NotificationData\NotificationDataBodyField;
use App\GraphQL\Definition\Fields\User\Notification\NotificationData\NotificationDataImageField;
use App\GraphQL\Definition\Fields\User\Notification\NotificationData\NotificationDataTitleField;
use App\GraphQL\Definition\Types\BaseType;
class NotificationDataType extends BaseType
{
/**
* The description of the type.
*/
public function description(): string
{
return 'Represents the JSON data of the notification';
}
/**
* The fields of the type.
*
* @return Field[]
*/
public function fieldClasses(): array
{
return [
new NotificationDataTitleField(),
new NotificationDataBodyField(),
new NotificationDataImageField(),
];
}
}
@@ -13,8 +13,9 @@ class ImageableUnion 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 'Imageable';
}
+2 -1
View File
@@ -12,8 +12,9 @@ class LikedUnion 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 'Liked';
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Definition\Unions;
use App\GraphQL\Definition\Types\BaseType;
use App\GraphQL\Definition\Types\User\Notification\ExternalProfileSyncedNotificationType;
use App\Notifications\ExternalProfileSyncedNotification;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use RuntimeException;
class NotificationUnion extends BaseUnion
{
/**
* The name of the union type.
* By default, it will be the class name.
*/
public function getName(): string
{
return 'Notification';
}
/**
* The description of the union type.
*/
public function description(): string
{
return 'Represents the notification types.';
}
/**
* The types that this union can resolve to.
*
* @return BaseType[]
*/
public function baseTypes(): array
{
return [
new ExternalProfileSyncedNotificationType(),
];
}
public function resolveType($value): Type
{
$type = match ($value->type) {
ExternalProfileSyncedNotification::class => new ExternalProfileSyncedNotificationType(),
default => throw new RuntimeException("Type not specified for notification {$value->type}"),
};
return GraphQL::type($type->getName());
}
}
@@ -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';
}
@@ -23,6 +23,6 @@ class MorphManyRelation extends Relation
*/
public function paginationType(): PaginationType
{
return PaginationType::CONNECTION;
return PaginationType::PAGINATOR;
}
}
+5 -2
View File
@@ -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);
+1 -2
View File
@@ -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;
}
/**
+20
View File
@@ -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');
}
}
@@ -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(),
];
}
}
+2
View File
@@ -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));
+7 -4
View File
@@ -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,
],
@@ -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,
];
}
}