Skip to content

Implement Core IssueQuery Builder with Fluent Filtering API #20

@jordanpartridge

Description

@jordanpartridge

Summary

Implement a fluent IssueQuery builder that provides an expressive, Laravel-style API for filtering and querying GitHub issues. This should follow the established patterns from conduit-ui/repo (RepositoryQuery, BranchQuery).

Requirements

Query Building Pattern

use ConduitUI\Issue\Facades\Issues;

// Basic queries
Issues::forRepo('owner/repo')
    ->whereOpen()
    ->get();

Issues::forRepo('owner/repo')
    ->whereClosed()
    ->get();

// Label filtering
Issues::forRepo('owner/repo')
    ->whereLabel('bug')
    ->get();

Issues::forRepo('owner/repo')
    ->whereLabels(['bug', 'priority-high'])
    ->get();

// Assignee filtering
Issues::forRepo('owner/repo')
    ->assignedTo('username')
    ->get();

Issues::forRepo('owner/repo')
    ->whereUnassigned()
    ->get();

// Author filtering
Issues::forRepo('owner/repo')
    ->createdBy('username')
    ->get();

// Time-based filtering
Issues::forRepo('owner/repo')
    ->createdAfter(now()->subWeek())
    ->get();

Issues::forRepo('owner/repo')
    ->updatedBefore(now()->subDays(90))
    ->get();

Issues::forRepo('owner/repo')
    ->older(days: 90)
    ->get();

// Sorting
Issues::forRepo('owner/repo')
    ->orderByCreated('desc')
    ->get();

Issues::forRepo('owner/repo')
    ->orderByUpdated('asc')
    ->get();

// Pagination
Issues::forRepo('owner/repo')
    ->perPage(50)
    ->page(2)
    ->get();

// Complex queries
Issues::forRepo('owner/repo')
    ->whereOpen()
    ->whereLabel('bug')
    ->assignedTo('jordan')
    ->createdAfter(now()->subWeek())
    ->orderByCreated('desc')
    ->get();

Implementation Details

Class Structure:

  • ConduitUI\Issue\Services\IssueQuery
  • Constructor should accept GitHub $github (from conduit-ui/connector)
  • All query methods return $this for chaining
  • Final get() method returns Collection of Issue DTOs

Query Methods:

  • forRepo(string $fullName): self - Set repository context
  • whereOpen(): self - Filter to open issues
  • whereClosed(): self - Filter to closed issues
  • whereState(string $state): self - Filter by state (open|closed|all)
  • whereLabel(string $label): self - Filter by single label
  • whereLabels(array $labels): self - Filter by multiple labels
  • assignedTo(string $username): self - Filter by assignee
  • whereUnassigned(): self - Filter to unassigned issues
  • createdBy(string $username): self - Filter by author
  • mentioning(string $username): self - Filter by mentioned user
  • createdAfter(Carbon|string $date): self - Filter by creation date
  • updatedBefore(Carbon|string $date): self - Filter by update date
  • older(int $days): self - Convenience method for stale issues
  • orderBy(string $field, string $direction = 'desc'): self - Sort results
  • orderByCreated(string $direction = 'desc'): self - Convenience method
  • orderByUpdated(string $direction = 'desc'): self - Convenience method
  • perPage(int $perPage): self - Set pagination limit
  • page(int $page): self - Set page number
  • get(): Collection - Execute query and return results
  • count(): int - Get count of matching issues
  • exists(): bool - Check if any issues match
  • first(): ?Issue - Get first matching issue

Protected Helper Methods:

  • buildEndpoint(): string - Build API endpoint
  • buildParams(): array - Build query parameters

GitHub API Mapping

Reference: https://docs.github.com/en/rest/issues/issues#list-repository-issues

Query Parameters:

  • state → open, closed, all (default: open)
  • labels → comma-separated list
  • assignee → username or "none"
  • creator → username
  • mentioned → username
  • since → ISO 8601 timestamp
  • sort → created, updated, comments (default: created)
  • direction → asc, desc (default: desc)
  • per_page → 1-100 (default: 30)
  • page → page number

Return Type

use ConduitUI\Issue\Data\Issue;
use Illuminate\Support\Collection;

/** @return Collection<int, Issue> */
public function get(): Collection

Testing Requirements

Unit Tests:

  • Test each filter method sets correct internal state
  • Test method chaining returns self
  • Test buildParams() generates correct query parameters
  • Test buildEndpoint() generates correct API path

Integration Tests:

  • Test get() returns Collection of Issue DTOs
  • Test filtering by labels
  • Test filtering by assignee
  • Test time-based filtering
  • Test sorting
  • Test pagination
  • Test complex multi-filter queries

Related Files to Reference

From conduit-ui/repo:

  • src/Services/RepositoryQuery.php - Query pattern
  • src/Services/BranchQuery.php - Advanced filtering example
  • src/Services/CommitQuery.php - Time-based filtering

Acceptance Criteria

  • IssueQuery class implements all query methods
  • All methods return self for chaining
  • get() method returns Collection of Issue DTOs
  • Query parameters correctly map to GitHub API
  • Time-based filtering handles Carbon and string dates
  • Full test coverage (unit + integration)
  • Follows existing conduit-ui patterns
  • Type hints and return types on all methods
  • PHPDoc blocks on all public methods

Dependencies

Technical Notes

Pattern Consistency:
Follow the established patterns from conduit-ui/repo:

  1. Constructor injection of GitHub client
  2. Fluent method chaining
  3. Protected state properties
  4. Single get() execution method
  5. Helper methods for building endpoint/params

Date Handling:

use Carbon\Carbon;

protected function formatDate(Carbon|string $date): string
{
    return $date instanceof Carbon 
        ? $date->toIso8601String()
        : Carbon::parse($date)->toIso8601String();
}

Examples

Triage Bot:

Issues::forRepo('laravel/framework')
    ->whereOpen()
    ->whereUnassigned()
    ->whereLabel('bug')
    ->createdAfter(now()->subDay())
    ->get()
    ->each(fn($issue) => triageIssue($issue));

Stale Issue Detection:

$staleIssues = Issues::forRepo('owner/repo')
    ->whereOpen()
    ->older(days: 90)
    ->get();

Team Dashboard:

$myIssues = Issues::forRepo('owner/repo')
    ->whereOpen()
    ->assignedTo('jordan')
    ->orderByUpdated('asc')
    ->get();

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions