Collapse profile group contribution queries

This commit is contained in:
2026-08-30 07:58:38 +02:00
parent dba1f73e01
commit 7805baa17d
2 changed files with 256 additions and 30 deletions
+86 -30
View File
@@ -1499,7 +1499,7 @@ class ProfileController extends Controller
private function buildGroupContributionHistory(User $user): array private function buildGroupContributionHistory(User $user): array
{ {
return GroupContributorStat::query() $stats = GroupContributorStat::query()
->with(['group.owner.profile']) ->with(['group.owner.profile'])
->where('user_id', $user->id) ->where('user_id', $user->id)
->whereHas('group', fn ($query) => $query ->whereHas('group', fn ($query) => $query
@@ -1508,29 +1508,60 @@ class ProfileController extends Controller
->orderByDesc('release_count') ->orderByDesc('release_count')
->orderByDesc('credited_artworks_count') ->orderByDesc('credited_artworks_count')
->limit(8) ->limit(8)
->get();
$groupIds = $stats->pluck('group_id')->map(static fn ($id): int => (int) $id)->values()->all();
if ($groupIds === []) {
return [];
}
$membershipsByGroup = GroupMember::query()
->whereIn('group_id', $groupIds)
->where('user_id', $user->id)
->where('status', Group::STATUS_ACTIVE)
->get() ->get()
->map(function (GroupContributorStat $stat) use ($user): ?array { ->keyBy('group_id');
$releaseRolesByGroup = GroupReleaseContributor::query()
->join('group_releases as bulk_releases', 'bulk_releases.id', '=', 'group_release_contributors.group_release_id')
->where('group_release_contributors.user_id', $user->id)
->whereIn('bulk_releases.group_id', $groupIds)
->whereNull('bulk_releases.deleted_at')
->orderBy('group_release_contributors.id')
->get([
'bulk_releases.group_id',
'group_release_contributors.role_label',
])
->groupBy('group_id');
$projectRolesByGroup = GroupProjectMember::query()
->join('group_projects as bulk_projects', 'bulk_projects.id', '=', 'group_project_members.group_project_id')
->where('group_project_members.user_id', $user->id)
->whereIn('bulk_projects.group_id', $groupIds)
->whereNull('bulk_projects.deleted_at')
->orderBy('group_project_members.id')
->get([
'bulk_projects.group_id',
'group_project_members.role_label',
])
->groupBy('group_id');
$recentReleasesByGroup = $this->loadRecentGroupReleaseTitles($user, $groupIds);
return $stats
->map(function (GroupContributorStat $stat) use ($user, $membershipsByGroup, $releaseRolesByGroup, $projectRolesByGroup, $recentReleasesByGroup): ?array {
$group = $stat->group; $group = $stat->group;
if (! $group) { if (! $group) {
return null; return null;
} }
$member = GroupMember::query() $groupId = (int) $group->id;
->where('group_id', $group->id) $member = $membershipsByGroup->get($groupId);
->where('user_id', $user->id)
->where('status', Group::STATUS_ACTIVE)
->first();
$roleLabels = collect() $roleLabels = collect()
->merge(GroupReleaseContributor::query() ->merge($releaseRolesByGroup->get($groupId, collect())->pluck('role_label'))
->where('user_id', $user->id) ->merge($projectRolesByGroup->get($groupId, collect())->pluck('role_label'))
->whereHas('release', fn ($query) => $query->where('group_id', $group->id))
->pluck('role_label'))
->merge(GroupProjectMember::query()
->where('user_id', $user->id)
->whereHas('project', fn ($query) => $query->where('group_id', $group->id))
->pluck('role_label'))
->filter() ->filter()
->map(fn ($label): string => trim((string) $label)) ->map(fn ($label): string => trim((string) $label))
->filter() ->filter()
@@ -1538,21 +1569,7 @@ class ProfileController extends Controller
->values() ->values()
->all(); ->all();
$recentReleaseTitles = GroupRelease::query() $recentReleaseTitles = $recentReleasesByGroup->get($groupId, collect())->all();
->where('group_id', $group->id)
->where('visibility', GroupRelease::VISIBILITY_PUBLIC)
->where(function ($query) use ($user): void {
$query->where('lead_user_id', $user->id)
->orWhere('created_by_user_id', $user->id)
->orWhereHas('contributorLinks', fn ($contributorQuery) => $contributorQuery->where('user_id', $user->id));
})
->orderByDesc('released_at')
->latest('updated_at')
->limit(3)
->pluck('title')
->map(fn ($title): string => (string) $title)
->values()
->all();
$joinedAt = $group->isOwnedBy($user) $joinedAt = $group->isOwnedBy($user)
? $group->created_at?->toISOString() ? $group->created_at?->toISOString()
@@ -1589,6 +1606,45 @@ class ProfileController extends Controller
->all(); ->all();
} }
/**
* Load the same top-three public releases per group as the previous
* per-group LIMIT query, while keeping the existing two-column ordering.
*
* @param array<int, int> $groupIds
* @return \Illuminate\Support\Collection<int, \Illuminate\Support\Collection<int, string>>
*/
private function loadRecentGroupReleaseTitles(User $user, array $groupIds): \Illuminate\Support\Collection
{
$eligibleReleases = GroupRelease::query()
->whereIn('group_id', $groupIds)
->where('visibility', GroupRelease::VISIBILITY_PUBLIC)
->where(function ($query) use ($user): void {
$query->where('lead_user_id', $user->id)
->orWhere('created_by_user_id', $user->id)
->orWhereHas('contributorLinks', fn ($contributorQuery) => $contributorQuery->where('user_id', $user->id));
})
->select(['id', 'group_id', 'title', 'released_at', 'updated_at']);
$ranked = DB::query()
->fromSub($eligibleReleases, 'eligible_group_releases')
->select([
'group_id',
'title',
'released_at',
'updated_at',
])
->selectRaw('ROW_NUMBER() OVER (PARTITION BY group_id ORDER BY released_at DESC, updated_at DESC) as release_rank');
return DB::query()
->fromSub($ranked, 'ranked_group_releases')
->where('release_rank', '<=', 3)
->orderBy('group_id')
->orderBy('release_rank')
->get(['group_id', 'title'])
->groupBy('group_id')
->map(fn ($rows) => $rows->pluck('title')->map(fn ($title): string => (string) $title)->values());
}
private function resolveFavouriteTable(): ?string private function resolveFavouriteTable(): ?string
{ {
foreach (['artwork_favourites', 'user_favorites', 'artworks_favourites', 'favourites'] as $table) { foreach (['artwork_favourites', 'user_favorites', 'artworks_favourites', 'favourites'] as $table) {
@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
use App\Http\Controllers\User\ProfileController;
use App\Models\Group;
use App\Models\GroupContributorStat;
use App\Models\GroupMember;
use App\Models\GroupProject;
use App\Models\GroupProjectMember;
use App\Models\GroupRelease;
use App\Models\GroupReleaseContributor;
use App\Models\User;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB;
function invokeGroupContributionHistory(User $user): array
{
$method = new ReflectionMethod(ProfileController::class, 'buildGroupContributionHistory');
$method->setAccessible(true);
return $method->invoke(app(ProfileController::class), $user);
}
function makeGroupRelease(Group $group, User $creator, string $title, CarbonImmutable $releasedAt, string $visibility = GroupRelease::VISIBILITY_PUBLIC): GroupRelease
{
return GroupRelease::create([
'group_id' => $group->id,
'title' => $title,
'slug' => strtolower(str_replace(' ', '-', $title)) . '-' . uniqid(),
'status' => GroupRelease::STATUS_RELEASED,
'current_stage' => GroupRelease::STAGE_RELEASED,
'visibility' => $visibility,
'released_at' => $releasedAt,
'published_at' => $releasedAt,
'created_by_user_id' => $creator->id,
]);
}
it('preserves contribution output semantics while loading group data in bulk', function (): void {
$user = User::factory()->create();
$owner = User::factory()->create();
$group = Group::factory()->create([
'owner_user_id' => $owner->id,
'name' => 'Visible Contribution Group',
'slug' => 'visible-contribution-group-' . uniqid(),
]);
GroupContributorStat::create([
'group_id' => $group->id,
'user_id' => $user->id,
'credited_artworks_count' => 7,
'release_count' => 5,
'project_count' => 3,
'review_actions_count' => 2,
]);
GroupMember::create([
'group_id' => $group->id,
'user_id' => $user->id,
'role' => Group::ROLE_EDITOR,
'status' => Group::STATUS_ACTIVE,
]);
$project = GroupProject::create([
'group_id' => $group->id,
'title' => 'Visible Project',
'slug' => 'visible-project-' . uniqid(),
'status' => GroupProject::STATUS_ACTIVE,
'visibility' => GroupProject::VISIBILITY_PUBLIC,
'created_by_user_id' => $owner->id,
]);
GroupProjectMember::create([
'group_project_id' => $project->id,
'user_id' => $user->id,
'role_label' => 'Project Lead',
'is_lead' => true,
]);
$newest = makeGroupRelease($group, $user, 'Newest Release', CarbonImmutable::parse('2026-08-20 12:00:00'));
$middle = makeGroupRelease($group, $owner, 'Middle Release', CarbonImmutable::parse('2026-08-19 12:00:00'));
$oldest = makeGroupRelease($group, $owner, 'Oldest Release', CarbonImmutable::parse('2026-08-18 12:00:00'));
makeGroupRelease($group, $owner, 'Hidden Release', CarbonImmutable::parse('2026-08-21 12:00:00'), GroupRelease::VISIBILITY_PRIVATE);
GroupReleaseContributor::create([
'group_release_id' => $middle->id,
'user_id' => $user->id,
'role_label' => 'Release Contributor',
'sort_order' => 0,
]);
GroupReleaseContributor::create([
'group_release_id' => $oldest->id,
'user_id' => $user->id,
'role_label' => 'Project Lead',
'sort_order' => 1,
]);
$result = invokeGroupContributionHistory($user);
expect($result)->toHaveCount(1);
expect($result[0]['group'])->toMatchArray([
'id' => $group->id,
'name' => 'Visible Contribution Group',
'slug' => $group->slug,
]);
expect($result[0]['joined_at'])->toBeString();
expect($result[0]['counts'])->toBe([
'credited_artworks' => 7,
'releases' => 5,
'projects' => 3,
'review_actions' => 2,
]);
expect($result[0]['role_labels'])->toBe(['Release Contributor', 'Project Lead']);
expect($result[0]['recent_release_titles'])->toBe(['Newest Release', 'Middle Release', 'Oldest Release']);
expect($result[0]['role'])->toBe(Group::ROLE_EDITOR);
expect($result[0]['trusted_indicator'])->toBeFalse();
});
it('keeps public visibility filtering and owner role semantics', function (): void {
$user = User::factory()->create();
$ownerGroup = Group::factory()->create([
'owner_user_id' => $user->id,
'slug' => 'owner-contribution-group-' . uniqid(),
]);
$privateGroup = Group::factory()->private()->create([
'slug' => 'private-contribution-group-' . uniqid(),
]);
GroupContributorStat::create(['group_id' => $ownerGroup->id, 'user_id' => $user->id]);
GroupContributorStat::create(['group_id' => $privateGroup->id, 'user_id' => $user->id, 'release_count' => 99]);
$result = invokeGroupContributionHistory($user);
expect($result)->toHaveCount(1)
->and($result[0]['group']['id'])->toBe($ownerGroup->id)
->and($result[0]['role'])->toBe(Group::ROLE_OWNER)
->and($result[0]['joined_at'])->toBeString();
});
it('keeps contribution-history query count bounded as group count grows', function (): void {
$counts = [];
foreach ([0, 1, 2, 4, 8] as $groupCount) {
$user = User::factory()->create();
for ($index = 0; $index < $groupCount; $index++) {
$group = Group::factory()->create([
'slug' => 'query-count-group-' . $groupCount . '-' . $index . '-' . uniqid(),
]);
GroupContributorStat::create([
'group_id' => $group->id,
'user_id' => $user->id,
'credited_artworks_count' => $groupCount - $index,
'release_count' => $groupCount - $index,
]);
}
DB::flushQueryLog();
DB::enableQueryLog();
invokeGroupContributionHistory($user);
$counts[$groupCount] = count(DB::getQueryLog());
DB::disableQueryLog();
}
expect($counts)->toBe([
0 => 1,
1 => 8,
2 => 8,
4 => 8,
8 => 8,
]);
});