Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions src/Contracts/PullRequestQueryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

declare(strict_types=1);

namespace ConduitUI\Pr\Contracts;

use ConduitUI\Pr\PullRequest;

interface PullRequestQueryInterface
{
/**
* Set the repository context
*/
public function repository(string $repository): self;

/**
* Filter by state
*/
public function state(string $state): self;

/**
* Filter for open pull requests
*/
public function open(): self;

/**
* Filter for closed pull requests
*/
public function closed(): self;

/**
* Filter for all pull requests (open and closed)
*/
public function all(): self;

/**
* Filter for merged pull requests
*/
public function whereMerged(): self;

/**
* Filter for draft pull requests
*/
public function whereDraft(): self;

/**
* Filter by base branch
*/
public function whereBase(string $branch): self;

/**
* Filter by head branch
*/
public function whereHead(string $branch): self;

/**
* Filter by author username
*/
public function author(string $author): self;

/**
* Filter by label
*/
public function label(string $label): self;

/**
* Order by a specific field
*/
public function orderBy(string $sort, string $direction = 'desc'): self;

/**
* Limit the number of results
*/
public function take(int $limit): self;

/**
* Set the page number for pagination
*/
public function page(int $page): self;

/**
* Execute the query and get results
*
* @return array<int, PullRequest>
*/
public function get(): array;

/**
* Get the first result or null
*/
public function first(): ?PullRequest;

/**
* Count the results
*/
public function count(): int;
}
64 changes: 60 additions & 4 deletions src/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
namespace ConduitUI\Pr;

use ConduitUi\GitHubConnector\Connector;
use ConduitUI\Pr\Contracts\PullRequestQueryInterface;
use ConduitUI\Pr\DataTransferObjects\PullRequest as PullRequestData;
use ConduitUI\Pr\Requests\ListPullRequests;

class QueryBuilder
class QueryBuilder implements PullRequestQueryInterface
{
protected ?string $owner = null;

Expand Down Expand Up @@ -74,6 +75,38 @@ public function label(string $label): self
return $this;
}

public function whereMerged(): self
{
// Note: GitHub API doesn't have a direct 'merged' filter
// This will need to be filtered client-side after fetching
$this->filters['_merged'] = true;

return $this;
}

public function whereDraft(): self
{
// Note: GitHub API doesn't have a direct 'draft' filter
// This will need to be filtered client-side after fetching
$this->filters['_draft'] = true;

return $this;
}

public function whereBase(string $branch): self
{
$this->filters['base'] = $branch;

return $this;
}

public function whereHead(string $branch): self
{
$this->filters['head'] = $branch;

return $this;
}

public function orderBy(string $sort, string $direction = 'desc'): self
{
$this->sort = $sort;
Expand Down Expand Up @@ -108,7 +141,19 @@ public function get(): array
$owner = $this->owner;
$repo = $this->repo;

$params = array_merge($this->filters, [
// Separate client-side filters from API filters
$clientSideFilters = [];
$apiFilters = [];

foreach ($this->filters as $key => $value) {
if (str_starts_with($key, '_')) {
$clientSideFilters[$key] = $value;
} else {
$apiFilters[$key] = $value;
}
}

$params = array_merge($apiFilters, [
'sort' => $this->sort,
'direction' => $this->direction,
'per_page' => $this->limit ?? 30,
Expand All @@ -125,7 +170,7 @@ public function get(): array
$params
));

return array_values(array_map(
$results = array_map(
/**
* @param array<string, mixed> $data
*/
Expand All @@ -136,7 +181,18 @@ public function get(): array
PullRequestData::fromArray($data) // @phpstan-ignore-line
),
$response->json()
));
);

// Apply client-side filters
if (isset($clientSideFilters['_merged'])) {
$results = array_filter($results, fn (PullRequest $pr) => $pr->data->isMerged());
}

if (isset($clientSideFilters['_draft'])) {
$results = array_filter($results, fn (PullRequest $pr) => $pr->data->isDraft());
}

return array_values($results);
}

public function first(): ?PullRequest
Expand Down
99 changes: 99 additions & 0 deletions tests/Unit/QueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,102 @@ function createMockPrData(): array
expect($results)->toBeArray()
->and($results)->toHaveCount(1);
});

it('can filter by merged state', function () {
$mergedPrData = createMockPrData();
$mergedPrData['merged_at'] = '2025-01-02T00:00:00Z';

$connector = createQueryBuilderConnector([$mergedPrData]);
$builder = new QueryBuilder($connector);

$result = $builder->repository('owner/repo')->whereMerged();

expect($result)->toBeInstanceOf(QueryBuilder::class);
});

it('can filter by draft state', function () {
$draftPrData = createMockPrData();
$draftPrData['draft'] = true;

$connector = createQueryBuilderConnector([$draftPrData]);
$builder = new QueryBuilder($connector);

$result = $builder->repository('owner/repo')->whereDraft();

expect($result)->toBeInstanceOf(QueryBuilder::class);
});

it('can filter by base branch', function () {
$connector = createQueryBuilderConnector([createMockPrData()]);
$builder = new QueryBuilder($connector);

$result = $builder->repository('owner/repo')->whereBase('main');

expect($result)->toBeInstanceOf(QueryBuilder::class);
});

it('can filter by head branch', function () {
$connector = createQueryBuilderConnector([createMockPrData()]);
$builder = new QueryBuilder($connector);

$result = $builder->repository('owner/repo')->whereHead('user:feature-branch');

expect($result)->toBeInstanceOf(QueryBuilder::class);
});

it('implements PullRequestQueryInterface', function () {
$connector = createQueryBuilderConnector([]);
$builder = new QueryBuilder($connector);

expect($builder)->toBeInstanceOf(\ConduitUI\Pr\Contracts\PullRequestQueryInterface::class);
});

it('can chain new filters with existing filters', function () {
$connector = createQueryBuilderConnector([createMockPrData()]);
$builder = new QueryBuilder($connector);

$results = $builder
->repository('owner/repo')
->whereBase('main')
->whereHead('user:feature')
->whereDraft()
->author('testuser')
->get();

expect($results)->toBeArray();
});

it('filters merged pull requests client-side', function () {
$mergedPrData = createMockPrData();
$mergedPrData['merged_at'] = '2025-01-02T00:00:00Z';

$openPrData = createMockPrData();
$openPrData['number'] = 2;

$connector = createQueryBuilderConnector([$mergedPrData, $openPrData]);
$builder = new QueryBuilder($connector);

$results = $builder->repository('owner/repo')->all()->whereMerged()->get();

expect($results)->toBeArray()
->and($results)->toHaveCount(1)
->and($results[0]->data->isMerged())->toBeTrue();
});

it('filters draft pull requests client-side', function () {
$draftPrData = createMockPrData();
$draftPrData['draft'] = true;

$nonDraftPrData = createMockPrData();
$nonDraftPrData['number'] = 2;
$nonDraftPrData['draft'] = false;

$connector = createQueryBuilderConnector([$draftPrData, $nonDraftPrData]);
$builder = new QueryBuilder($connector);

$results = $builder->repository('owner/repo')->whereDraft()->get();

expect($results)->toBeArray()
->and($results)->toHaveCount(1)
->and($results[0]->data->isDraft())->toBeTrue();
});
Loading