Skip to content

Commit 56abc35

Browse files
committed
WIP
1 parent 67cf018 commit 56abc35

File tree

46 files changed

+1762
-21
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1762
-21
lines changed

src/Drivers/EloquentEntitySet.php

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Flat3\Lodata\EntitySet;
2525
use Flat3\Lodata\EntityType;
2626
use Flat3\Lodata\EnumerationType;
27+
use Flat3\Lodata\Exception\Protocol\BadRequestException;
2728
use Flat3\Lodata\Exception\Protocol\ConfigurationException;
2829
use Flat3\Lodata\Exception\Protocol\InternalServerErrorException;
2930
use Flat3\Lodata\Exception\Protocol\NotFoundException;
@@ -45,6 +46,7 @@
4546
use Flat3\Lodata\Interfaces\EntitySet\ReadInterface;
4647
use Flat3\Lodata\Interfaces\EntitySet\RelationshipInterface;
4748
use Flat3\Lodata\Interfaces\EntitySet\SearchInterface;
49+
use Flat3\Lodata\Interfaces\EntitySet\TokenPaginationInterface;
4850
use Flat3\Lodata\Interfaces\EntitySet\UpdateInterface;
4951
use Flat3\Lodata\Interfaces\TransactionInterface;
5052
use Flat3\Lodata\NavigationBinding;
@@ -69,6 +71,7 @@
6971
use Illuminate\Support\Facades\App;
7072
use Illuminate\Support\Facades\DB;
7173
use Illuminate\Support\Str;
74+
use JsonException;
7275
use ReflectionAttribute;
7376
use ReflectionClass;
7477
use ReflectionException;
@@ -80,7 +83,7 @@
8083
* Eloquent Entity Set
8184
* @package Flat3\Lodata\Drivers
8285
*/
83-
class EloquentEntitySet extends EntitySet implements CountInterface, CreateInterface, DeleteInterface, ExpandInterface, FilterInterface, OrderByInterface, PaginationInterface, QueryInterface, ReadInterface, SearchInterface, TransactionInterface, UpdateInterface, ComputeInterface, RelationshipInterface
86+
class EloquentEntitySet extends EntitySet implements CountInterface, CreateInterface, DeleteInterface, ExpandInterface, FilterInterface, OrderByInterface, PaginationInterface, QueryInterface, ReadInterface, SearchInterface, TransactionInterface, UpdateInterface, ComputeInterface, RelationshipInterface, TokenPaginationInterface
8487
{
8588
use SQLConnection;
8689
use SQLOrderBy;
@@ -100,6 +103,7 @@ class EloquentEntitySet extends EntitySet implements CountInterface, CreateInter
100103
* @var int $chunkSize
101104
*/
102105
public static $chunkSize = 1000;
106+
protected $useTokenPagination = false;
103107

104108
public function __construct(string $model, ?EntityType $entityType = null)
105109
{
@@ -398,6 +402,30 @@ public function query(): Generator
398402
$chunkSize = $this->getTop()->getValue();
399403
}
400404

405+
if ($this->useTokenPagination && !$this->getSkip()->hasValue() && $this->getOrderBy()->hasValue()) {
406+
$result = null;
407+
$results = $builder->limit($chunkSize)->get();
408+
409+
foreach ($results as $result) {
410+
yield $this->modelToEntity($result);
411+
}
412+
413+
$this->getSkip()->clearValue();
414+
$this->getSkipToken()->clearValue();
415+
416+
if ($result && $results->count() === $chunkSize) {
417+
$skipToken = [];
418+
419+
foreach ($this->getOrderBy()->getSortOrders() as $sortOrder) {
420+
$skipToken[$sortOrder[0]] = $result->getAttribute($this->getPropertySourceName($this->getType()->getProperty($sortOrder[0])));
421+
}
422+
423+
$this->getSkipToken()->setValue(base64_encode(json_encode($skipToken)));
424+
}
425+
426+
return;
427+
}
428+
401429
while (true) {
402430
$offset = (($page++ - 1) * $chunkSize) + $skipValue;
403431
$results = $builder->offset($offset)->limit($chunkSize)->get();
@@ -444,6 +472,49 @@ protected function configureBuilder($builder)
444472

445473
$builder->skip($this->getSkip()->getValue());
446474
}
475+
476+
if (!$this->getSkip()->hasValue()) {
477+
if (!$this->getOrderBy()->hasValue() && $this->getType()->getKey()) {
478+
$this->getOrderBy()->setValue(sprintf('%s asc', $this->getType()->getKey()));
479+
}
480+
481+
if ($this->getSkipToken()->hasValue()) {
482+
$builder->where(function ($builder) {
483+
try {
484+
$skipToken = json_decode(
485+
base64_decode($this->getSkipToken()->getValue()),
486+
true,
487+
512,
488+
JSON_THROW_ON_ERROR
489+
);
490+
} catch (JsonException $e) {
491+
throw new BadRequestException('invalid_skip_token', 'Invalid skip token');
492+
}
493+
494+
$orderBys = $this->getOrderBy()->getSortOrders();
495+
496+
for ($outer = 0; $outer < count($orderBys); $outer++) {
497+
$builder->orWhere(function ($builder) use ($orderBys, $skipToken, $outer) {
498+
for ($inner = 0; $inner < $outer; $inner++) {
499+
$col = $orderBys[$inner][0];
500+
$builder->where(
501+
$this->getPropertySourceName($this->getType()->getProperty($col)),
502+
'=',
503+
$skipToken[$col]
504+
);
505+
}
506+
507+
$col = $orderBys[$outer][0];
508+
$builder->where(
509+
$this->getPropertySourceName($this->getType()->getProperty($col)),
510+
strtolower($orderBys[$outer][1]) === 'asc' ? '>' : '<',
511+
$skipToken[$col]
512+
);
513+
});
514+
}
515+
});
516+
}
517+
}
447518
}
448519

449520
/**
@@ -892,4 +963,11 @@ public function discover(): self
892963

893964
return $this;
894965
}
966+
967+
public function useTokenPagination($use = true): self
968+
{
969+
$this->useTokenPagination = $use;
970+
971+
return $this;
972+
}
895973
}

src/EntitySet.php

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -821,24 +821,22 @@ public function addTrailingMetadata(Transaction $transaction, MetadataContainer
821821
$paginationParams = [];
822822

823823
if ($top->hasValue() && ($count === null || $top->getValue() < $count)) {
824-
switch (true) {
825-
case $this instanceof TokenPaginationInterface:
826-
$skipToken = $transaction->getSkipToken();
827-
828-
if ($skipToken->hasValue()) {
829-
$paginationParams[$top::param] = $top->getValue();
830-
$paginationParams[$skipToken::param] = $skipToken->getValue();
831-
}
832-
break;
833-
834-
case $this instanceof PaginationInterface:
835-
$skip = $transaction->getSkip();
836-
837-
if ($skip->hasValue() && ($count === null || $skip->getValue() < $count)) {
838-
$paginationParams[$top::param] = $top->getValue();
839-
$paginationParams[$skip::param] = $skip->getValue();
840-
}
841-
break;
824+
if ($this instanceof TokenPaginationInterface) {
825+
$skipToken = $transaction->getSkipToken();
826+
827+
if ($skipToken->hasValue()) {
828+
$paginationParams[$top::param] = $top->getValue();
829+
$paginationParams[$skipToken::param] = $skipToken->getValue();
830+
}
831+
}
832+
833+
if ($this instanceof PaginationInterface) {
834+
$skip = $transaction->getSkip();
835+
836+
if ($skip->hasValue() && ($count === null || $skip->getValue() < $count)) {
837+
$paginationParams[$top::param] = $top->getValue();
838+
$paginationParams[$skip::param] = $skip->getValue();
839+
}
842840
}
843841
}
844842

src/PathSegment/OpenAPI.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Flat3\Lodata\Interfaces\ContextInterface;
2626
use Flat3\Lodata\Interfaces\EntitySet\DeleteInterface;
2727
use Flat3\Lodata\Interfaces\EntitySet\ExpandInterface;
28+
use Flat3\Lodata\Interfaces\EntitySet\PaginationInterface;
2829
use Flat3\Lodata\Interfaces\EntitySet\QueryInterface;
2930
use Flat3\Lodata\Interfaces\EntitySet\TokenPaginationInterface;
3031
use Flat3\Lodata\Interfaces\EntitySet\UpdateInterface;
@@ -871,9 +872,12 @@ protected function generateQueryRoutes(
871872

872873
if ($annotations->supportsTop()) {
873874
$parameters[] = ['$ref' => '#/components/parameters/top'];
875+
874876
if ($entitySet instanceof TokenPaginationInterface) {
875877
$parameters[] = ['$ref' => '#/components/parameters/skiptoken'];
876-
} else {
878+
}
879+
880+
if ($entitySet instanceof PaginationInterface) {
877881
$parameters[] = ['$ref' => '#/components/parameters/skip'];
878882
}
879883
}

tests/Pagination/EloquentTest.php

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Flat3\Lodata\Tests\Pagination;
66

77
use Flat3\Lodata\Drivers\EloquentEntitySet;
8+
use Flat3\Lodata\Facades\Lodata;
89
use Flat3\Lodata\Tests\Drivers\WithEloquentDriver;
910
use Flat3\Lodata\Tests\Helpers\Request;
1011
use Flat3\Lodata\Tests\Laravel\Models\Pet;
@@ -75,7 +76,7 @@ public function test_chunk_skip_top($chunkSize)
7576
->filter("type eq 'dog'")
7677
->count('true')
7778
->path($this->petEntitySetPath),
78-
PHP_INT_MAX, '@nextLink', 40-2,
79+
PHP_INT_MAX, '@nextLink', 40 - 2,
7980
);
8081
}
8182

@@ -114,4 +115,84 @@ public function test_chunk_skip($chunkSize)
114115
PHP_INT_MAX, '@nextLink', 40 - 15,
115116
);
116117
}
118+
119+
public function test_cursor()
120+
{
121+
Lodata::getEntitySet('Pets')->useTokenPagination();
122+
123+
$this->assertPaginationSequence(
124+
(new Request)
125+
->top('5')
126+
->count('true')
127+
->path($this->petEntitySetPath),
128+
PHP_INT_MAX, '@nextLink', 40,
129+
);
130+
}
131+
132+
public function test_cursor_2()
133+
{
134+
Lodata::getEntitySet('Pets')->useTokenPagination();
135+
136+
$this->assertPaginationSequence(
137+
(new Request)
138+
->top('7')
139+
->count('true')
140+
->path($this->petEntitySetPath),
141+
PHP_INT_MAX, '@nextLink', 40,
142+
);
143+
}
144+
145+
public function test_cursor_filter()
146+
{
147+
Lodata::getEntitySet('Pets')->useTokenPagination();
148+
149+
$this->assertPaginationSequence(
150+
(new Request)
151+
->top('5')
152+
->orderby('id asc')
153+
->filter("type eq 'dog'")
154+
->count('true')
155+
->path($this->petEntitySetPath),
156+
PHP_INT_MAX, '@nextLink', 40,
157+
);
158+
}
159+
160+
public function test_cursor_multiple_orders()
161+
{
162+
Lodata::getEntitySet('Pets')->useTokenPagination();
163+
164+
$this->assertPaginationSequence(
165+
(new Request)
166+
->top('5')
167+
->orderby('id desc, type asc')
168+
->count('true')
169+
->path($this->petEntitySetPath),
170+
PHP_INT_MAX, '@nextLink', 40,
171+
);
172+
}
173+
174+
public function test_bad_cursor()
175+
{
176+
Lodata::getEntitySet('Pets')->useTokenPagination();
177+
178+
$this->assertBadRequest(
179+
(new Request)
180+
->top('5')
181+
->skiptoken('broken')
182+
->count('true')
183+
->path($this->petEntitySetPath),
184+
);
185+
}
186+
187+
public function test_cursor_no_top()
188+
{
189+
Lodata::getEntitySet('Pets')->useTokenPagination();
190+
191+
$this->assertPaginationSequence(
192+
(new Request)
193+
->count('true')
194+
->path($this->petEntitySetPath),
195+
PHP_INT_MAX, '@nextLink', 40,
196+
);
197+
}
117198
}

0 commit comments

Comments
 (0)