diff --git a/CHANGELOG.md b/CHANGELOG.md index 78ebc50..c6a9fd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.6.1 + +- Bumped `Version::API_SPEC_VERSION` to the Apify OpenAPI spec `v2-2026-09-10T091137Z`. This + version formally documents the `X-Apify-Pagination-*` response headers (including the + previously-undocumented `X-Apify-Pagination-Desc`) on all offset-paginated list endpoints and + the `offset`/`limit`/`desc` query parameters on the webhook dispatches list; this client already + implemented all of those. +- `DatasetClient::listItems()` now prefers the `X-Apify-Pagination-Desc` response header over the + requested `desc` option when reporting `PaginationList::isDesc()`, matching the reference JS + client's `_createPaginationList` and the header newly documented in the spec above. + ## 0.6.0 - Synced to Apify OpenAPI spec `v2-2026-09-02T154542Z`. diff --git a/src/Resource/DatasetClient.php b/src/Resource/DatasetClient.php index 1861926..635dd5a 100644 --- a/src/Resource/DatasetClient.php +++ b/src/Resource/DatasetClient.php @@ -100,7 +100,10 @@ public function listItems(?DatasetListItemsOptions $options = null): PaginationL $this->headerInt($response, 'X-Apify-Pagination-Offset', 0), $this->headerInt($response, 'X-Apify-Pagination-Limit', $count), $count, - $options->desc ?? false, + // Prefer the server-reported X-Apify-Pagination-Desc header (matches the reference JS + // client's `_createPaginationList`); fall back to the requested option when the header + // is absent, e.g. against an older API version that predates it. + $this->headerBool($response, 'X-Apify-Pagination-Desc', $options->desc ?? false), ); } @@ -215,4 +218,10 @@ private function headerInt(ResponseInterface $response, string $name, int $fallb $value = $response->getHeaderLine($name); return $value === '' ? $fallback : (int) $value; } + + private function headerBool(ResponseInterface $response, string $name, bool $fallback): bool + { + $value = $response->getHeaderLine($name); + return $value === '' ? $fallback : $value === 'true'; + } } diff --git a/src/Version.php b/src/Version.php index 102349f..1018efa 100644 --- a/src/Version.php +++ b/src/Version.php @@ -17,13 +17,13 @@ final class Version * The semantic version of this client library (see https://semver.org/). * Changes to the public interface other than additive ones are considered breaking changes. */ - public const CLIENT_VERSION = '0.6.0'; + public const CLIENT_VERSION = '0.6.1'; /** * The version of the Apify OpenAPI specification this client was generated and verified * against. Corresponds to the {@code info.version} field of the Apify OpenAPI document. */ - public const API_SPEC_VERSION = 'v2-2026-09-02T154542Z'; + public const API_SPEC_VERSION = 'v2-2026-09-10T091137Z'; private function __construct() { diff --git a/tests/Integration/DatasetIntegrationTest.php b/tests/Integration/DatasetIntegrationTest.php index 2635832..9af3144 100644 --- a/tests/Integration/DatasetIntegrationTest.php +++ b/tests/Integration/DatasetIntegrationTest.php @@ -105,6 +105,8 @@ public function testDatasetCrudFlow(): void self::assertSame(3, $page->getCount()); self::assertCount(3, $page->getItems()); self::assertSame(1, $page->getItems()[0]['n']); + // isDesc() is sourced from the server's X-Apify-Pagination-Desc response header. + self::assertFalse($page->isDesc()); $csv = $dataset->downloadItems(DownloadItemsFormat::CSV, new DatasetDownloadOptions(bom: true)); self::assertStringContainsString('url', $csv); diff --git a/tests/Unit/HttpClientTest.php b/tests/Unit/HttpClientTest.php index 62581bd..c53f60e 100644 --- a/tests/Unit/HttpClientTest.php +++ b/tests/Unit/HttpClientTest.php @@ -7,6 +7,7 @@ use Apify\Client\ApifyClient; use Apify\Client\Exception\ApifyApiException; use Apify\Client\Internal\Json; +use Apify\Client\Options\DatasetListItemsOptions; use PHPUnit\Framework\TestCase; final class HttpClientTest extends TestCase @@ -129,6 +130,44 @@ public function testDatasetItemsUseHeaderPagination(): void self::assertSame(42, $page->getTotal()); self::assertSame(3, $page->getCount()); self::assertSame(1, $page->getItems()[0]['n']); + self::assertFalse($page->isDesc()); + } + + public function testDatasetItemsDescHeaderTakesPrecedenceOverOption(): void + { + // The server-reported X-Apify-Pagination-Desc header must win over the requested option + // (matches the reference JS client), so a page always reflects what the server actually did. + $transport = (new MockTransport())->queueResponse( + 200, + Json::encode([['n' => 1]]), + ['X-Apify-Pagination-Desc' => 'true'] + ); + $page = $this->client($transport)->dataset('ds1')->listItems(new DatasetListItemsOptions(desc: false)); + + self::assertTrue($page->isDesc()); + } + + public function testDatasetItemsDescHeaderFalseTakesPrecedenceOverOption(): void + { + // Symmetric case: the header must be genuinely *parsed*, not just checked for presence. + // A "false" header must win over a contradicting `desc: true` option, so this fails if + // headerBool() ever regresses to presence-only ("header is set, so trust the option"). + $transport = (new MockTransport())->queueResponse( + 200, + Json::encode([['n' => 1]]), + ['X-Apify-Pagination-Desc' => 'false'] + ); + $page = $this->client($transport)->dataset('ds1')->listItems(new DatasetListItemsOptions(desc: true)); + + self::assertFalse($page->isDesc()); + } + + public function testDatasetItemsDescFallsBackToOptionWhenHeaderMissing(): void + { + $transport = (new MockTransport())->queueResponse(200, Json::encode([['n' => 1]])); + $page = $this->client($transport)->dataset('ds1')->listItems(new DatasetListItemsOptions(desc: true)); + + self::assertTrue($page->isDesc()); } public function testValidateInputParsesBareObject(): void