diff --git a/config.php b/config.php index b6c31d206..708fa788e 100644 --- a/config.php +++ b/config.php @@ -77,5 +77,20 @@ 'api_secret', 'secret', ] - ] + ], + + /* + * Configuration for server-driven pagination + */ + 'pagination' => [ + /** + * The maximum page size this service will return, null for no limit + */ + 'max' => null, + + /** + * The default page size to use if the client does not request one, null for no default + */ + 'default' => 200, + ], ]; diff --git a/src/EntitySet.php b/src/EntitySet.php index bf00a8b98..295a19bf6 100644 --- a/src/EntitySet.php +++ b/src/EntitySet.php @@ -203,22 +203,12 @@ public function emitJson(Transaction $transaction): void $transaction = $this->transaction ?: $transaction; - // Validate $orderby - $orderby = $transaction->getOrderBy(); - $orderby->getSortOrders(); - - $top = $transaction->getTop(); - - $maxPageSize = $transaction->getPreferenceValue(Constants::maxPageSize); - if (!$top->hasValue() && $maxPageSize) { - $top->setValue($maxPageSize); - } - /** @var Generator $results */ $results = $this->query(); $transaction->outputJsonArrayStart(); + $top = $this->getTop(); $limit = $top->getValue(); while ($results->valid()) { @@ -257,11 +247,22 @@ public function get(Transaction $transaction, ?ContextInterface $context = null) Gate::query($this, $transaction)->ensure(); $top = $this->getTop(); - $maxPageSize = $transaction->getPreferenceValue(Constants::maxPageSize); - if (!$top->hasValue() && $maxPageSize) { - $transaction->preferenceApplied(Constants::maxPageSize, $maxPageSize); - $top->setValue($maxPageSize); + $defaultPageSize = config('lodata.pagination.default'); + $maxPageSize = config('lodata.pagination.max'); + $preferPageSize = $transaction->getPreferenceValue(Constants::maxPageSize); + $targetPageSize = $top->getValue() ?: $preferPageSize ?: $defaultPageSize; + + if ($maxPageSize && (!$targetPageSize || $targetPageSize > $maxPageSize)) { + $targetPageSize = $maxPageSize; + } + + if ($preferPageSize && $targetPageSize === $preferPageSize) { + $transaction->preferenceApplied(Constants::maxPageSize, (string) $targetPageSize); + } + + if ($targetPageSize !== $top->getValue()) { + $top->setValue((string) $targetPageSize); } $this->assertValidOrderBy(); @@ -740,7 +741,7 @@ public function addTrailingMetadata(Transaction $transaction, MetadataContainer $top = $transaction->getTop(); $paginationParams = []; - if ($top->hasValue()) { + if ($top->hasValue() && ($count === null || $top->getValue() < $count)) { switch (true) { case $this instanceof TokenPaginationInterface: $skipToken = $transaction->getSkipToken(); diff --git a/tests/Helpers/UseODataAssertions.php b/tests/Helpers/UseODataAssertions.php index 1316669c6..791e85b8d 100644 --- a/tests/Helpers/UseODataAssertions.php +++ b/tests/Helpers/UseODataAssertions.php @@ -121,4 +121,9 @@ protected function assertConflict(Request $request): TestResponse { return $this->assertODataError($request, Response::HTTP_CONFLICT); } + + protected function assertResultCount(TestResponse $response, int $count) + { + $this->assertEquals($count, count(json_decode($response->streamedContent())->value)); + } } \ No newline at end of file diff --git a/tests/Parser/LoopbackEntitySet.php b/tests/Parser/LoopbackEntitySet.php index 501dd38e3..89c84c951 100644 --- a/tests/Parser/LoopbackEntitySet.php +++ b/tests/Parser/LoopbackEntitySet.php @@ -9,7 +9,6 @@ use Flat3\Lodata\Expression\Node\Literal\Date; use Flat3\Lodata\Expression\Node\Literal\DateTimeOffset; use Flat3\Lodata\Expression\Node\Literal\Duration; -use Flat3\Lodata\Expression\Node\Literal\Guid; use Flat3\Lodata\Expression\Node\Literal\String_; use Flat3\Lodata\Expression\Node\Literal\TimeOfDay; use Flat3\Lodata\Expression\Node\Operator\Comparison\And_; diff --git a/tests/Protocol/AuthorizationTest.php b/tests/Protocol/AuthorizationTest.php index 89dea4257..bcaaf7bb2 100644 --- a/tests/Protocol/AuthorizationTest.php +++ b/tests/Protocol/AuthorizationTest.php @@ -15,7 +15,6 @@ public function setUp(): void { parent::setUp(); config(['lodata.authorization' => true]); - config(['lodata.readonly' => false]); } public function test_no_authorization() diff --git a/tests/Protocol/BatchJSONTest.php b/tests/Protocol/BatchJSONTest.php index ecf58dfe3..61beb1228 100644 --- a/tests/Protocol/BatchJSONTest.php +++ b/tests/Protocol/BatchJSONTest.php @@ -2,7 +2,6 @@ namespace Flat3\Lodata\Tests\Protocol; -use Flat3\Lodata\Drivers\SQLEntitySet; use Flat3\Lodata\Facades\Lodata; use Flat3\Lodata\Operation; use Flat3\Lodata\Tests\Drivers\WithSQLDriver; diff --git a/tests/Protocol/BatchMultipartTest.php b/tests/Protocol/BatchMultipartTest.php index 273ad44d4..6c549f909 100644 --- a/tests/Protocol/BatchMultipartTest.php +++ b/tests/Protocol/BatchMultipartTest.php @@ -2,7 +2,6 @@ namespace Flat3\Lodata\Tests\Protocol; -use Flat3\Lodata\Drivers\SQLEntitySet; use Flat3\Lodata\Facades\Lodata; use Flat3\Lodata\Operation; use Flat3\Lodata\Tests\Drivers\WithSQLDriver; diff --git a/tests/Protocol/MaxPageSizeTest.php b/tests/Protocol/MaxPageSizeTest.php index 1bc93318b..7b7719f78 100644 --- a/tests/Protocol/MaxPageSizeTest.php +++ b/tests/Protocol/MaxPageSizeTest.php @@ -27,4 +27,149 @@ public function test_uses_odata_maxpagesize_preference() ->header('Prefer', 'odata.maxpagesize=1') ); } + + public function test_unlimited() + { + config([ + 'lodata.pagination.default' => null, + 'lodata.pagination.max' => null + ]); + + $response = $this->req( + (new Request) + ->path($this->entitySetPath) + ); + + $this->assertResponseHeaderSnapshot($response); + $this->assertResultCount($response, 5); + } + + public function test_unlimited_uses_top() + { + config([ + 'lodata.pagination.default' => null, + 'lodata.pagination.max' => null + ]); + + $response = $this->req( + (new Request) + ->top(2) + ->path($this->entitySetPath) + ); + + $this->assertResponseHeaderSnapshot($response); + $this->assertResultCount($response, 2); + } + + public function test_unlimited_uses_maxpagesize() + { + config([ + 'lodata.pagination.default' => null, + 'lodata.pagination.max' => null + ]); + + $response = $this->req( + (new Request) + ->preference('maxpagesize', 2) + ->path($this->entitySetPath) + ); + + $this->assertResponseHeaderSnapshot($response); + $this->assertResultCount($response, 2); + } + + public function test_limits_to_default_if_unspecified() + { + config(['lodata.pagination.default' => 1]); + + $response = $this->req( + (new Request) + ->path($this->entitySetPath) + ); + + $this->assertResponseHeaderSnapshot($response); + $this->assertResultCount($response, 1); + } + + public function test_limits_to_max_if_unspecified() + { + config(['lodata.pagination.max' => 1]); + + $response = $this->req( + (new Request) + ->path($this->entitySetPath) + ); + + $this->assertResponseHeaderSnapshot($response); + $this->assertResultCount($response, 1); + } + + public function test_preference_overrides_default() + { + config(['lodata.pagination.default' => 1]); + + $response = $this->req( + (new Request) + ->preference('maxpagesize', 2) + ->path($this->entitySetPath) + ); + + $this->assertResponseHeaderSnapshot($response); + $this->assertResultCount($response, 2); + } + + public function test_preference_does_not_override_max() + { + config(['lodata.pagination.max' => 1]); + + $response = $this->req( + (new Request) + ->preference('maxpagesize', 2) + ->path($this->entitySetPath) + ); + + $this->assertResponseHeaderSnapshot($response); + $this->assertResultCount($response, 1); + } + + public function test_top_overrides_default() + { + config(['lodata.pagination.default' => 1]); + + $response = $this->req( + (new Request) + ->top(2) + ->path($this->entitySetPath) + ); + + $this->assertResponseHeaderSnapshot($response); + $this->assertResultCount($response, 2); + } + + public function test_top_does_not_override_max() + { + config(['lodata.pagination.max' => 1]); + + $response = $this->req( + (new Request) + ->top(2) + ->path($this->entitySetPath) + ); + + $this->assertResponseHeaderSnapshot($response); + $this->assertResultCount($response, 1); + } + + public function test_top_overrides_maxpagesize() + { + $response = $this->req( + (new Request) + ->top(1) + ->preference('maxpagesize', 2) + ->path($this->entitySetPath) + ); + + $this->assertResponseHeaderSnapshot($response); + $this->assertResultCount($response, 1); + } } diff --git a/tests/Protocol/__snapshots__/MaxPageSizeTest__test_limits_to_default_if_unspecified__1.yml b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_limits_to_default_if_unspecified__1.yml new file mode 100644 index 000000000..c2c0c1bc7 --- /dev/null +++ b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_limits_to_default_if_unspecified__1.yml @@ -0,0 +1,5 @@ +headers: + cache-control: ['no-cache, private'] + content-type: [application/json] + odata-version: ['4.01'] +status: 200 diff --git a/tests/Protocol/__snapshots__/MaxPageSizeTest__test_limits_to_max_if_unspecified__1.yml b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_limits_to_max_if_unspecified__1.yml new file mode 100644 index 000000000..c2c0c1bc7 --- /dev/null +++ b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_limits_to_max_if_unspecified__1.yml @@ -0,0 +1,5 @@ +headers: + cache-control: ['no-cache, private'] + content-type: [application/json] + odata-version: ['4.01'] +status: 200 diff --git a/tests/Protocol/__snapshots__/MaxPageSizeTest__test_preference_does_not_override_max__1.yml b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_preference_does_not_override_max__1.yml new file mode 100644 index 000000000..c2c0c1bc7 --- /dev/null +++ b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_preference_does_not_override_max__1.yml @@ -0,0 +1,5 @@ +headers: + cache-control: ['no-cache, private'] + content-type: [application/json] + odata-version: ['4.01'] +status: 200 diff --git a/tests/Protocol/__snapshots__/MaxPageSizeTest__test_preference_overrides_default__1.yml b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_preference_overrides_default__1.yml new file mode 100644 index 000000000..a5efb3bdb --- /dev/null +++ b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_preference_overrides_default__1.yml @@ -0,0 +1,7 @@ +headers: + cache-control: ['no-cache, private'] + content-type: [application/json] + odata-version: ['4.01'] + preference-applied: [maxpagesize=2] + vary: [prefer] +status: 200 diff --git a/tests/Protocol/__snapshots__/MaxPageSizeTest__test_top_does_not_override_max__1.yml b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_top_does_not_override_max__1.yml new file mode 100644 index 000000000..c2c0c1bc7 --- /dev/null +++ b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_top_does_not_override_max__1.yml @@ -0,0 +1,5 @@ +headers: + cache-control: ['no-cache, private'] + content-type: [application/json] + odata-version: ['4.01'] +status: 200 diff --git a/tests/Protocol/__snapshots__/MaxPageSizeTest__test_top_overrides_default__1.yml b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_top_overrides_default__1.yml new file mode 100644 index 000000000..c2c0c1bc7 --- /dev/null +++ b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_top_overrides_default__1.yml @@ -0,0 +1,5 @@ +headers: + cache-control: ['no-cache, private'] + content-type: [application/json] + odata-version: ['4.01'] +status: 200 diff --git a/tests/Protocol/__snapshots__/MaxPageSizeTest__test_top_overrides_maxpagesize__1.yml b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_top_overrides_maxpagesize__1.yml new file mode 100644 index 000000000..c2c0c1bc7 --- /dev/null +++ b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_top_overrides_maxpagesize__1.yml @@ -0,0 +1,5 @@ +headers: + cache-control: ['no-cache, private'] + content-type: [application/json] + odata-version: ['4.01'] +status: 200 diff --git a/tests/Protocol/__snapshots__/MaxPageSizeTest__test_unlimited__1.yml b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_unlimited__1.yml new file mode 100644 index 000000000..c2c0c1bc7 --- /dev/null +++ b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_unlimited__1.yml @@ -0,0 +1,5 @@ +headers: + cache-control: ['no-cache, private'] + content-type: [application/json] + odata-version: ['4.01'] +status: 200 diff --git a/tests/Protocol/__snapshots__/MaxPageSizeTest__test_unlimited_uses_maxpagesize__1.yml b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_unlimited_uses_maxpagesize__1.yml new file mode 100644 index 000000000..a5efb3bdb --- /dev/null +++ b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_unlimited_uses_maxpagesize__1.yml @@ -0,0 +1,7 @@ +headers: + cache-control: ['no-cache, private'] + content-type: [application/json] + odata-version: ['4.01'] + preference-applied: [maxpagesize=2] + vary: [prefer] +status: 200 diff --git a/tests/Protocol/__snapshots__/MaxPageSizeTest__test_unlimited_uses_top__1.yml b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_unlimited_uses_top__1.yml new file mode 100644 index 000000000..c2c0c1bc7 --- /dev/null +++ b/tests/Protocol/__snapshots__/MaxPageSizeTest__test_unlimited_uses_top__1.yml @@ -0,0 +1,5 @@ +headers: + cache-control: ['no-cache, private'] + content-type: [application/json] + odata-version: ['4.01'] +status: 200 diff --git a/tests/TestCase.php b/tests/TestCase.php index 2d3809fdd..0d67bee6b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -5,7 +5,6 @@ use Faker\Factory; use Faker\Generator as FakerGenerator; use Flat3\Lodata\DeclaredProperty; -use Flat3\Lodata\Drivers\SQLEntitySet; use Flat3\Lodata\DynamicProperty; use Flat3\Lodata\EntitySet; use Flat3\Lodata\EntityType; @@ -80,11 +79,15 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase public function getEnvironmentSetUp($app) { - config(['database.redis.client' => 'mock']); - config(['filesystems.disks.testing' => ['driver' => 'vfs']]); - config(['lodata.readonly' => false]); - config(['lodata.disk' => 'testing']); - config(['lodata.streaming' => false]); + config([ + 'database.redis.client' => 'mock', + 'filesystems.disks.testing' => ['driver' => 'vfs'], + 'lodata.readonly' => false, + 'lodata.disk' => 'testing', + 'lodata.streaming' => false, + 'lodata.pagination.max' => null, + 'lodata.pagination.default' => 200, + ]); $app->register(RedisMockServiceProvider::class);