diff --git a/src/Models/Activity.php b/src/Models/Activity.php index 197d845..1770264 100644 --- a/src/Models/Activity.php +++ b/src/Models/Activity.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Support\Carbon; +use Illuminate\Support\Str; /** * @property string $event @@ -57,6 +58,46 @@ public function scopeEvent(Builder $query, string|array $event): Builder return $query->whereIn('event', (array) $event); } + public function scopeEventPrefix(Builder $query, string|array $prefix): Builder + { + return $query->where(function (Builder $query) use ($prefix): void { + foreach ((array) $prefix as $eventPrefix) { + $query->orWhere('event', 'like', Str::finish($eventPrefix, '.').'%'); + } + }); + } + + public function scopeForActor(Builder $query, Model $actor): Builder + { + return $query + ->where('actor_type', $actor->getMorphClass()) + ->where('actor_id', $actor->getKey()); + } + + public function scopeForSubject(Builder $query, Model $subject): Builder + { + return $query + ->where('subject_type', $subject->getMorphClass()) + ->where('subject_id', $subject->getKey()); + } + + public function scopeForParent(Builder $query, Model $parent): Builder + { + return $query + ->where('parent_type', $parent->getMorphClass()) + ->where('parent_id', $parent->getKey()); + } + + public function scopeOccurredSince(Builder $query, Carbon|string $since): Builder + { + return $query->where('occurred_at', '>=', $since); + } + + public function scopeLatestActivity(Builder $query): Builder + { + return $query->latest('occurred_at')->latest($this->getQualifiedKeyName()); + } + public function tenantColumn(): string { return config('filament-activity.tenant.foreign_key', 'tenant_id'); diff --git a/tests/Feature/ActivityRecorderTest.php b/tests/Feature/ActivityRecorderTest.php index 862a0a9..8135956 100644 --- a/tests/Feature/ActivityRecorderTest.php +++ b/tests/Feature/ActivityRecorderTest.php @@ -45,6 +45,43 @@ expect(Activity::query()->forTenant($teamA)->sole()->id)->toBe($teamAActivity->id); }); +it('scopes activities by actor subject parent event prefix and recency', function (): void { + $actor = User::query()->create(['name' => 'Ada']); + $otherActor = User::query()->create(['name' => 'Grace']); + $post = Post::query()->create(['name' => 'Topic']); + $otherPost = Post::query()->create(['name' => 'Other topic']); + $parent = Post::query()->create(['name' => 'Parent topic']); + $recorder = app(ActivityRecorder::class); + + $matchingActivity = $recorder->record( + event: 'forum.post.created', + subject: $post, + actor: $actor, + parent: $parent, + ); + $matchingActivity->forceFill(['occurred_at' => now()->subHour()])->save(); + + $recorder->record('course.lesson.completed', $post, actor: $actor, parent: $parent); + $recorder->record('forum.post.created', $otherPost, actor: $actor, parent: $parent); + $recorder->record('forum.post.created', $post, actor: $otherActor, parent: $parent); + + expect(Activity::query()->forActor($actor)->count())->toBe(3) + ->and(Activity::query()->forSubject($post)->count())->toBe(3) + ->and(Activity::query()->forParent($parent)->count())->toBe(4) + ->and(Activity::query()->eventPrefix('forum')->count())->toBe(3) + ->and(Activity::query()->eventPrefix(['forum.post'])->count())->toBe(3) + ->and(Activity::query()->occurredSince(now()->subMinutes(30))->count())->toBe(3) + ->and(Activity::query() + ->forActor($actor) + ->forSubject($post) + ->forParent($parent) + ->eventPrefix('forum') + ->occurredSince(now()->subHours(2)) + ->sole() + ->id + )->toBe($matchingActivity->id); +}); + it('exposes subject activities through the trait', function (): void { $post = Post::query()->create(['name' => 'Topic']);