Batch-load comment reaction counts on artwork comments.
Avoid per-comment reaction queries on the deferred comments API by aggregating counts in one service call.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\ReactionType;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkComment;
|
||||
use App\Models\CommentReaction;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function m18b1ArtworkCommentTree(int $topLevelCount = 2): array
|
||||
{
|
||||
$artwork = Artwork::factory()->create();
|
||||
$author = User::factory()->create();
|
||||
$viewer = User::factory()->create();
|
||||
$other = User::factory()->create();
|
||||
$comments = [];
|
||||
|
||||
for ($i = 0; $i < $topLevelCount; $i++) {
|
||||
$comment = ArtworkComment::factory()->create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'user_id' => $author->id,
|
||||
'parent_id' => null,
|
||||
]);
|
||||
$reply = ArtworkComment::factory()->create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'user_id' => $other->id,
|
||||
'parent_id' => $comment->id,
|
||||
]);
|
||||
|
||||
CommentReaction::create([
|
||||
'comment_id' => $comment->id,
|
||||
'user_id' => $other->id,
|
||||
'reaction' => ReactionType::Heart->value,
|
||||
]);
|
||||
CommentReaction::create([
|
||||
'comment_id' => $reply->id,
|
||||
'user_id' => $viewer->id,
|
||||
'reaction' => ReactionType::Fire->value,
|
||||
]);
|
||||
CommentReaction::create([
|
||||
'comment_id' => $reply->id,
|
||||
'user_id' => $other->id,
|
||||
'reaction' => ReactionType::Clap->value,
|
||||
]);
|
||||
|
||||
$comments[] = [$comment, $reply];
|
||||
}
|
||||
|
||||
return [$artwork, $viewer, $comments];
|
||||
}
|
||||
|
||||
function m18b1ReactionQueryCount(array $queries): int
|
||||
{
|
||||
return collect($queries)
|
||||
->filter(fn (array $query): bool => str_contains(strtolower((string) $query['query']), 'comment_reactions'))
|
||||
->count();
|
||||
}
|
||||
|
||||
it('includes bulk reaction totals for guests with one reaction query', function (): void {
|
||||
[$artwork, , $comments] = m18b1ArtworkCommentTree();
|
||||
|
||||
DB::enableQueryLog();
|
||||
DB::flushQueryLog();
|
||||
|
||||
$response = $this->getJson("/api/artworks/{$artwork->id}/comments")->assertOk();
|
||||
$payload = $response->json('data');
|
||||
$withoutReactions = json_decode(json_encode($payload, JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR);
|
||||
$stripReactions = function (array &$items) use (&$stripReactions): void {
|
||||
foreach ($items as &$item) {
|
||||
unset($item['reactions']);
|
||||
if (isset($item['replies']) && is_array($item['replies'])) {
|
||||
$stripReactions($item['replies']);
|
||||
}
|
||||
}
|
||||
};
|
||||
$stripReactions($withoutReactions);
|
||||
$commentCount = 0;
|
||||
$countComments = function (array $items) use (&$countComments, &$commentCount): void {
|
||||
foreach ($items as $item) {
|
||||
$commentCount++;
|
||||
if (isset($item['replies']) && is_array($item['replies'])) {
|
||||
$countComments($item['replies']);
|
||||
}
|
||||
}
|
||||
};
|
||||
$countComments($payload);
|
||||
|
||||
fwrite(STDERR, sprintf(
|
||||
"M18B1 guest payload bytes: before=%d after=%d added=%d comments/replies=%d\n",
|
||||
strlen(json_encode($withoutReactions, JSON_THROW_ON_ERROR)),
|
||||
strlen(json_encode($payload, JSON_THROW_ON_ERROR)),
|
||||
strlen(json_encode($payload, JSON_THROW_ON_ERROR)) - strlen(json_encode($withoutReactions, JSON_THROW_ON_ERROR)),
|
||||
$commentCount,
|
||||
));
|
||||
|
||||
expect(m18b1ReactionQueryCount(DB::getQueryLog()))->toBe(1)
|
||||
->and($payload[0])->toHaveKey('reactions')
|
||||
->and($payload[0]['reactions'][ReactionType::Heart->value]['count'])->toBe(1)
|
||||
->and($payload[0]['reactions'][ReactionType::Heart->value]['mine'])->toBeFalse()
|
||||
->and($payload[0]['replies'][0]['reactions'][ReactionType::Fire->value]['count'])->toBe(1)
|
||||
->and(count($comments))->toBe(2);
|
||||
|
||||
DB::disableQueryLog();
|
||||
});
|
||||
|
||||
it('includes viewer reaction state for comments and replies with one extra query', function (): void {
|
||||
[$artwork, $viewer] = m18b1ArtworkCommentTree();
|
||||
|
||||
DB::enableQueryLog();
|
||||
DB::flushQueryLog();
|
||||
|
||||
$response = $this->actingAs($viewer)
|
||||
->getJson("/api/artworks/{$artwork->id}/comments")
|
||||
->assertOk();
|
||||
$payload = $response->json('data');
|
||||
|
||||
expect(m18b1ReactionQueryCount(DB::getQueryLog()))->toBe(2)
|
||||
->and($payload[0]['reactions'][ReactionType::Heart->value]['mine'])->toBeFalse()
|
||||
->and($payload[0]['replies'][0]['reactions'][ReactionType::Fire->value]['mine'])->toBeTrue();
|
||||
|
||||
DB::disableQueryLog();
|
||||
});
|
||||
|
||||
it('keeps reaction query count constant as the comment tree grows', function (): void {
|
||||
[$smallArtwork] = m18b1ArtworkCommentTree(2);
|
||||
[$largeArtwork] = m18b1ArtworkCommentTree(20);
|
||||
|
||||
DB::enableQueryLog();
|
||||
|
||||
DB::flushQueryLog();
|
||||
$this->getJson("/api/artworks/{$smallArtwork->id}/comments")->assertOk();
|
||||
$smallCount = m18b1ReactionQueryCount(DB::getQueryLog());
|
||||
|
||||
DB::flushQueryLog();
|
||||
$this->getJson("/api/artworks/{$largeArtwork->id}/comments")->assertOk();
|
||||
$largeCount = m18b1ReactionQueryCount(DB::getQueryLog());
|
||||
|
||||
DB::disableQueryLog();
|
||||
|
||||
expect($smallCount)->toBe(1)->and($largeCount)->toBe(1);
|
||||
});
|
||||
Reference in New Issue
Block a user