From 63cb73fe8d90978972af4cf305c66cb110f4f5a9 Mon Sep 17 00:00:00 2001 From: Kyrch Date: Fri, 19 Sep 2025 12:50:55 -0300 Subject: [PATCH] feat: groupships (#965) --- .../Resources/Wiki/Song/Performance.php | 2 + .../Tabs/Artist/Song/ArtistPerformanceTab.php | 4 +- app/GraphQL/Schema/Types/Wiki/ArtistType.php | 1 + app/GraphQL/Support/Filter/EqFilter.php | 2 +- app/GraphQL/Support/Filter/GreaterFilter.php | 2 +- app/GraphQL/Support/Filter/InFilter.php | 2 +- app/GraphQL/Support/Filter/LesserFilter.php | 2 +- app/GraphQL/Support/Filter/LikeFilter.php | 2 +- app/GraphQL/Support/Filter/NotInFilter.php | 2 +- app/Models/Wiki/Artist.php | 38 +++++++--- app/Providers/GraphQLServiceProvider.php | 13 ++++ composer.json | 1 + composer.lock | 69 ++++++++++++++++++- config/graphql.php | 2 +- 14 files changed, 123 insertions(+), 19 deletions(-) diff --git a/app/Filament/Resources/Wiki/Song/Performance.php b/app/Filament/Resources/Wiki/Song/Performance.php index 007e4af53..d580dff4d 100644 --- a/app/Filament/Resources/Wiki/Song/Performance.php +++ b/app/Filament/Resources/Wiki/Song/Performance.php @@ -135,6 +135,7 @@ class Performance extends BaseResource TextColumn::make('alias') ->label(__('filament.fields.membership.alias.name')) + ->hiddenOn(GroupPerformanceArtistRelationManager::class) ->state(function ($record) { if ($record->artist instanceof Membership) { return $record->artist->alias; @@ -145,6 +146,7 @@ class Performance extends BaseResource TextColumn::make('as') ->label(__('filament.fields.membership.as.name')) + ->hiddenOn(GroupPerformanceArtistRelationManager::class) ->state(function ($record) { if ($record->artist instanceof Membership) { return $record->artist->as; diff --git a/app/Filament/Tabs/Artist/Song/ArtistPerformanceTab.php b/app/Filament/Tabs/Artist/Song/ArtistPerformanceTab.php index 5aad6565e..7408c8afd 100644 --- a/app/Filament/Tabs/Artist/Song/ArtistPerformanceTab.php +++ b/app/Filament/Tabs/Artist/Song/ArtistPerformanceTab.php @@ -29,7 +29,7 @@ class ArtistPerformanceTab extends BaseTab return $query ->whereDoesntHave(Artist::RELATION_PERFORMANCES) ->whereDoesntHave(Artist::RELATION_MEMBERSHIPS_PERFORMANCES) - ->whereDoesntHave(Artist::RELATION_GROUPMEMBERSHIPS_PERFORMANCES); + ->whereDoesntHave(Artist::RELATION_GROUPSHIPS_PERFORMANCES); } public function getBadge(): int @@ -37,7 +37,7 @@ class ArtistPerformanceTab extends BaseTab return Artist::query() ->whereDoesntHave(Artist::RELATION_PERFORMANCES) ->whereDoesntHave(Artist::RELATION_MEMBERSHIPS_PERFORMANCES) - ->whereDoesntHave(Artist::RELATION_GROUPMEMBERSHIPS_PERFORMANCES) + ->whereDoesntHave(Artist::RELATION_GROUPSHIPS_PERFORMANCES) ->count(); } } diff --git a/app/GraphQL/Schema/Types/Wiki/ArtistType.php b/app/GraphQL/Schema/Types/Wiki/ArtistType.php index 38826a608..cf724ef28 100644 --- a/app/GraphQL/Schema/Types/Wiki/ArtistType.php +++ b/app/GraphQL/Schema/Types/Wiki/ArtistType.php @@ -45,6 +45,7 @@ class ArtistType extends EloquentType implements ReportableType new BelongsToManyRelation($this, ArtistType::class, Artist::RELATION_MEMBERS, ArtistMemberType::class), new MorphToManyRelation($this, ImageType::class, Artist::RELATION_IMAGES, ImageableType::class), new MorphToManyRelation($this, ExternalResourceType::class, Artist::RELATION_RESOURCES, ResourceableType::class), + new HasManyRelation(new MembershipType(), Artist::RELATION_GROUPSHIPS), new HasManyRelation(new MembershipType(), Artist::RELATION_MEMBERSHIPS), new MorphManyRelation(new PerformanceType(), Artist::RELATION_PERFORMANCES), ]; diff --git a/app/GraphQL/Support/Filter/EqFilter.php b/app/GraphQL/Support/Filter/EqFilter.php index 42008e989..424761480 100644 --- a/app/GraphQL/Support/Filter/EqFilter.php +++ b/app/GraphQL/Support/Filter/EqFilter.php @@ -19,7 +19,7 @@ class EqFilter extends Filter public function filter(Builder $builder, mixed $value): Builder { return $builder->where( - $this->field->getColumn(), + $builder->qualifyColumn($this->field->getColumn()), ComparisonOperator::EQ->value, $value, ); diff --git a/app/GraphQL/Support/Filter/GreaterFilter.php b/app/GraphQL/Support/Filter/GreaterFilter.php index 4fae4ac58..932b2f531 100644 --- a/app/GraphQL/Support/Filter/GreaterFilter.php +++ b/app/GraphQL/Support/Filter/GreaterFilter.php @@ -19,7 +19,7 @@ class GreaterFilter extends Filter public function filter(Builder $builder, mixed $value): Builder { return $builder->where( - $this->field->getColumn(), + $builder->qualifyColumn($this->field->getColumn()), ComparisonOperator::GT->value, $value, ); diff --git a/app/GraphQL/Support/Filter/InFilter.php b/app/GraphQL/Support/Filter/InFilter.php index 20d953c91..c8773b6bf 100644 --- a/app/GraphQL/Support/Filter/InFilter.php +++ b/app/GraphQL/Support/Filter/InFilter.php @@ -19,7 +19,7 @@ class InFilter extends Filter public function filter(Builder $builder, mixed $value): Builder { return $builder->whereIn( - $this->field->getColumn(), + $builder->qualifyColumn($this->field->getColumn()), $value, ); } diff --git a/app/GraphQL/Support/Filter/LesserFilter.php b/app/GraphQL/Support/Filter/LesserFilter.php index 3dcaa857b..f57d75e7b 100644 --- a/app/GraphQL/Support/Filter/LesserFilter.php +++ b/app/GraphQL/Support/Filter/LesserFilter.php @@ -19,7 +19,7 @@ class LesserFilter extends Filter public function filter(Builder $builder, mixed $value): Builder { return $builder->where( - $this->field->getColumn(), + $builder->qualifyColumn($this->field->getColumn()), ComparisonOperator::LT->value, $value, ); diff --git a/app/GraphQL/Support/Filter/LikeFilter.php b/app/GraphQL/Support/Filter/LikeFilter.php index 035310e58..bfef21c05 100644 --- a/app/GraphQL/Support/Filter/LikeFilter.php +++ b/app/GraphQL/Support/Filter/LikeFilter.php @@ -19,7 +19,7 @@ class LikeFilter extends Filter public function filter(Builder $builder, mixed $value): Builder { return $builder->where( - $this->field->getColumn(), + $builder->qualifyColumn($this->field->getColumn()), ComparisonOperator::LIKE->value, $value, ); diff --git a/app/GraphQL/Support/Filter/NotInFilter.php b/app/GraphQL/Support/Filter/NotInFilter.php index 57c747fdb..9e55ead08 100644 --- a/app/GraphQL/Support/Filter/NotInFilter.php +++ b/app/GraphQL/Support/Filter/NotInFilter.php @@ -19,7 +19,7 @@ class NotInFilter extends Filter public function filter(Builder $builder, mixed $value): Builder { return $builder->whereNotIn( - $this->field->getColumn(), + $builder->qualifyColumn($this->field->getColumn()), $value, ); } diff --git a/app/Models/Wiki/Artist.php b/app/Models/Wiki/Artist.php index 136195c53..334e0eb78 100644 --- a/app/Models/Wiki/Artist.php +++ b/app/Models/Wiki/Artist.php @@ -32,6 +32,8 @@ use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Collection; +use Staudenmeir\EloquentHasManyDeep\HasManyDeep; +use Staudenmeir\EloquentHasManyDeep\HasRelationships; /** * @property int $artist_id @@ -51,6 +53,7 @@ use Illuminate\Support\Collection; class Artist extends BaseModel implements HasImages, HasResources, SoftDeletable { use HasFactory; + use HasRelationships; use Reportable; use Searchable; use SoftDeletes; @@ -65,8 +68,9 @@ class Artist extends BaseModel implements HasImages, HasResources, SoftDeletable final public const RELATION_ANIME = 'songs.animethemes.anime'; final public const RELATION_ANIMETHEMES = 'songs.animethemes'; final public const RELATION_GROUPS = 'groups'; + final public const RELATION_GROUPSHIPS = 'groupships'; final public const RELATION_GROUP_PERFORMANCES = 'groupperformances'; - final public const RELATION_GROUPMEMBERSHIPS_PERFORMANCES = 'groupmemberships.performances'; + final public const RELATION_GROUPSHIPS_PERFORMANCES = 'groupships.performances'; final public const RELATION_IMAGES = 'images'; final public const RELATION_MEMBERS = 'members'; final public const RELATION_MEMBERSHIPS = 'memberships'; @@ -177,6 +181,8 @@ class Artist extends BaseModel implements HasImages, HasResources, SoftDeletable /** * @return BelongsToMany + * + * @deprecated */ public function songs(): BelongsToMany { @@ -195,15 +201,29 @@ class Artist extends BaseModel implements HasImages, HasResources, SoftDeletable return $this->morphMany(Performance::class, Performance::RELATION_ARTIST); } - public function groupperformances() + /** + * Relation for filament. + * Groups performances of the memberships of this group. + * + * @return HasManyDeep + */ + public function groupperformances(): HasManyDeep { - return $this->hasManyThrough( + $sub = Performance::query() + ->selectRaw('MAX(performance_id) as performance_id') + ->where(Performance::ATTRIBUTE_ARTIST_TYPE, Relation::getMorphAlias(Membership::class)) + ->groupBy(Performance::ATTRIBUTE_SONG); + + return $this->hasManyDeep( Performance::class, - Membership::class, - 'member_id', - 'artist_id' - )->select(['memberships.as', 'memberships.alias']) - ->where(Performance::ATTRIBUTE_ARTIST_TYPE, Relation::getMorphAlias(Membership::class)); + [Membership::class], + [Membership::ATTRIBUTE_ARTIST, [Performance::ATTRIBUTE_ARTIST_TYPE, Performance::ATTRIBUTE_ARTIST_ID]], + [Artist::ATTRIBUTE_ID, Membership::ATTRIBUTE_ID] + ) + ->joinSub($sub, 'latest_performance', function ($join) { + $join->on(new Performance()->qualifyColumn(Performance::ATTRIBUTE_ID), '=', 'latest_performance.performance_id'); + }) + ->where(new Performance()->qualifyColumn(Performance::ATTRIBUTE_ARTIST_TYPE), Relation::getMorphAlias(Membership::class)); } /** @@ -221,7 +241,7 @@ class Artist extends BaseModel implements HasImages, HasResources, SoftDeletable * * @return HasMany */ - public function groupmemberships(): HasMany + public function groupships(): HasMany { return $this->hasMany(Membership::class, Membership::ATTRIBUTE_ARTIST); } diff --git a/app/Providers/GraphQLServiceProvider.php b/app/Providers/GraphQLServiceProvider.php index 1be5f12e5..537a142d7 100644 --- a/app/Providers/GraphQLServiceProvider.php +++ b/app/Providers/GraphQLServiceProvider.php @@ -20,6 +20,8 @@ use App\Enums\Models\Wiki\ThemeType; use App\Enums\Models\Wiki\VideoOverlap; use App\Enums\Models\Wiki\VideoSource; use App\GraphQL\Schema\Enums\EnumType; +use GraphQL\Validator\DocumentValidator; +use GraphQL\Validator\Rules\QueryComplexity as BaseQueryComplexity; use Illuminate\Support\ServiceProvider; use Rebing\GraphQL\Support\Facades\GraphQL; @@ -27,6 +29,17 @@ class GraphQLServiceProvider extends ServiceProvider { public function boot(): void { + # Query Complexity rule on demand + DocumentValidator::addRule( + new class(250) extends BaseQueryComplexity + { + protected function isEnabled(): bool + { + return request()->ip() !== '127.0.0.1'; + } + } + ); + $this->bootEnums(); } diff --git a/composer.json b/composer.json index 03a5e10dc..f73157bcb 100644 --- a/composer.json +++ b/composer.json @@ -52,6 +52,7 @@ "spatie/db-dumper": "^3.8.0", "spatie/laravel-permission": "^6.21.0", "staudenmeir/belongs-to-through": "^2.17", + "staudenmeir/eloquent-has-many-deep": "1.21.1", "staudenmeir/laravel-adjacency-list": "^1.25.2", "symfony/http-client": "^6.4.25", "symfony/mailgun-mailer": "^6.4.24", diff --git a/composer.lock b/composer.lock index 337b20277..8cb561919 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": "97ba8049d40c8e062a7fd678627d7048", + "content-hash": "d1dbc28aa17e04138f3c631b222d3dea", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -8507,6 +8507,73 @@ ], "time": "2025-02-20T19:24:03+00:00" }, + { + "name": "staudenmeir/eloquent-has-many-deep", + "version": "v1.21.1", + "source": { + "type": "git", + "url": "https://github.com/staudenmeir/eloquent-has-many-deep.git", + "reference": "7810607379cef2e18accc75ee09903070178255d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staudenmeir/eloquent-has-many-deep/zipball/7810607379cef2e18accc75ee09903070178255d", + "reference": "7810607379cef2e18accc75ee09903070178255d", + "shasum": "" + }, + "require": { + "illuminate/database": "^12.0", + "php": "^8.2", + "staudenmeir/eloquent-has-many-deep-contracts": "^1.3" + }, + "require-dev": { + "awobaz/compoships": "^2.3", + "barryvdh/laravel-ide-helper": "^3.0", + "korridor/laravel-has-many-merged": "^1.2", + "larastan/larastan": "^3.0", + "laravel/framework": "^12.0", + "mockery/mockery": "^1.6", + "orchestra/testbench-core": "^10.0", + "phpunit/phpunit": "^11.0", + "staudenmeir/eloquent-json-relations": "^1.14", + "staudenmeir/laravel-adjacency-list": "^1.24" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Staudenmeir\\EloquentHasManyDeep\\IdeHelperServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Staudenmeir\\EloquentHasManyDeep\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonas Staudenmeir", + "email": "mail@jonas-staudenmeir.de" + } + ], + "description": "Laravel Eloquent HasManyThrough relationships with unlimited levels", + "support": { + "issues": "https://github.com/staudenmeir/eloquent-has-many-deep/issues", + "source": "https://github.com/staudenmeir/eloquent-has-many-deep/tree/v1.21.1" + }, + "funding": [ + { + "url": "https://paypal.me/JonasStaudenmeir", + "type": "custom" + } + ], + "time": "2025-06-29T13:53:15+00:00" + }, { "name": "staudenmeir/eloquent-has-many-deep-contracts", "version": "v1.3", diff --git a/config/graphql.php b/config/graphql.php index 50fac6d46..cbbe029a0 100644 --- a/config/graphql.php +++ b/config/graphql.php @@ -106,7 +106,7 @@ return [ * for details. Disabled by default. */ 'security' => [ - 'query_max_complexity' => 217, + 'query_max_complexity' => 0, 'query_max_depth' => 13, 'disable_introspection' => false, ],