From c34498a1c01a3ddac2ff5b5045f72da5cc2d27b9 Mon Sep 17 00:00:00 2001 From: Adam Anderly Date: Fri, 8 Sep 2023 11:25:19 -0500 Subject: [PATCH] Add support for odata paging using LazyCollection over @odata.nextLink (#142) * Add support for OData paging using LazyCollection over @odata.nextLink * Drop support for Laravel 5.8 for use of LazyCollection. * Add support for setting odata.maxpagesize through new pageSize method on Builder. --- composer.json | 2 +- src/Constants.php | 2 +- src/Entity.php | 125 +++++++++++---------- src/IODataClient.php | 54 +++++++++ src/ODataClient.php | 105 ++++++++++++++++-- src/ODataRequest.php | 39 ++++++- src/ODataResponse.php | 186 ++++++++++++++++--------------- src/Query/Builder.php | 58 +++++++++- src/Query/Grammar.php | 15 +++ tests/ODataClientTest.php | 211 ++++++++++++++++++++++++++++++++++-- tests/Query/BuilderTest.php | 4 +- 11 files changed, 628 insertions(+), 173 deletions(-) diff --git a/composer.json b/composer.json index d2a6ff5..b5c88f0 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "php": "^7.3 || ^8.0", "guzzlehttp/guzzle": "^7.0", "nesbot/carbon": "^2.0", - "illuminate/support": "^5.8 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0" + "illuminate/support": "^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0" }, "require-dev": { "phpunit/phpunit": "^9.0" diff --git a/src/Constants.php b/src/Constants.php index 733444e..b8dcfe9 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -19,7 +19,7 @@ class Constants { - const SDK_VERSION = '0.5.2'; + const SDK_VERSION = '0.6.7'; // ODATA Versions to be used when accessing the Web API (see: https://msdn.microsoft.com/en-us/library/gg334391.aspx) const MAX_ODATA_VERSION = '4.0'; diff --git a/src/Entity.php b/src/Entity.php index 3bc61bb..9a9d516 100644 --- a/src/Entity.php +++ b/src/Entity.php @@ -1,16 +1,18 @@ bootIfNotBooted(); @@ -210,7 +212,7 @@ function __construct($properties = array()) */ protected function bootIfNotBooted() { - if (! isset(static::$booted[static::class])) { + if (!isset(static::$booted[static::class])) { static::$booted[static::class] = true; // $this->fireModelEvent('booting', false); @@ -241,7 +243,7 @@ protected static function bootTraits() $class = static::class; foreach (class_uses_recursive($class) as $trait) { - if (method_exists($class, $method = 'boot'.class_basename($trait))) { + if (method_exists($class, $method = 'boot' . class_basename($trait))) { forward_static_call([$class, $method]); } } @@ -307,7 +309,7 @@ public function forceFill(array $properties) */ protected function fillableFromArray(array $properties) { - if (count($this->getFillable()) > 0 && ! static::$unguarded) { + if (count($this->getFillable()) > 0 && !static::$unguarded) { return array_intersect_key($properties, array_flip($this->getFillable())); } @@ -463,7 +465,7 @@ public function makeVisible($properties) { $this->hidden = array_diff($this->hidden, (array) $properties); - if (! empty($this->visible)) { + if (!empty($this->visible)) { $this->addVisible($properties); } @@ -699,7 +701,7 @@ public function isFillable($key) return false; } - return empty($this->getFillable()) && ! Str::startsWith($key, '_'); + return empty($this->getFillable()) && !Str::startsWith($key, '_'); } /** @@ -724,10 +726,10 @@ public function totallyGuarded() } /** - * Gets the property dictionary of the Entity - * - * @return array The list of properties - */ + * Gets the property dictionary of the Entity + * + * @return array The list of properties + */ public function getProperties() { return $this->properties; @@ -865,7 +867,7 @@ public function offsetUnset($offset): void */ public function __isset($key) { - return ! is_null($this->getProperty($key)); + return !is_null($this->getProperty($key)); } /** @@ -977,7 +979,7 @@ protected function castProperty($key, $value) case 'array': case 'json': return $this->fromJson($value); - //case 'collection': + //case 'collection': //return new BaseCollection($this->fromJson($value)); case 'date': return $this->asDate($value); @@ -1004,7 +1006,7 @@ public function setProperty($key, $value) // which simply lets the developers tweak the property as it is set on // the entity, such as "json_encoding" a listing of data for storage. if ($this->hasSetMutator($key)) { - $method = 'set'.Str::studly($key).'Property'; + $method = 'set' . Str::studly($key) . 'Property'; return $this->{$method}($value); } @@ -1016,7 +1018,7 @@ public function setProperty($key, $value) $value = $this->fromDateTime($value); } - if ($this->isJsonCastable($key) && ! is_null($value)) { + if ($this->isJsonCastable($key) && !is_null($value)) { $value = $this->asJson($value); } @@ -1040,7 +1042,7 @@ public function setProperty($key, $value) */ public function hasSetMutator($key) { - return method_exists($this, 'set'.Str::studly($key).'Property'); + return method_exists($this, 'set' . Str::studly($key) . 'Property'); } /** @@ -1086,7 +1088,8 @@ protected function asDateTime($value) // when checking the field. We will just return the DateTime right away. if ($value instanceof DateTimeInterface) { return Date::parse( - $value->format('Y-m-d H:i:s.u'), $value->getTimezone() + $value->format('Y-m-d H:i:s.u'), + $value->getTimezone() ); } // If this value is an integer, we will assume it is a UNIX timestamp's value @@ -1165,7 +1168,7 @@ protected function serializeDate(DateTimeInterface $date) */ protected function getDateFormat() { - return $this->dateFormat;// ?: $this->getConnection()->getQueryGrammar()->getDateFormat(); + return $this->dateFormat; // ?: $this->getConnection()->getQueryGrammar()->getDateFormat(); } /** @@ -1201,7 +1204,7 @@ protected function asJson($value) */ public function fromJson($value, $asObject = false) { - return json_decode($value, ! $asObject); + return json_decode($value, !$asObject); } /** @@ -1313,7 +1316,7 @@ public function propertiesToArray() protected function addDatePropertiesToArray(array $properties) { foreach ($this->getDates() as $key) { - if (! isset($properties[$key])) { + if (!isset($properties[$key])) { continue; } @@ -1338,7 +1341,7 @@ protected function addMutatedPropertiesToArray(array $properties, array $mutated // We want to spin through all the mutated properties for this model and call // the mutator for the properties. We cache off every mutated properties so // we don't have to constantly check on properties that actually change. - if (! array_key_exists($key, $properties)) { + if (!array_key_exists($key, $properties)) { continue; } @@ -1346,7 +1349,8 @@ protected function addMutatedPropertiesToArray(array $properties, array $mutated // mutated property's actual values. After we finish mutating each of the // properties we will return this final array of the mutated properties. $properties[$key] = $this->mutatePropertyForArray( - $key, $properties[$key] + $key, + $properties[$key] ); } @@ -1363,7 +1367,7 @@ protected function addMutatedPropertiesToArray(array $properties, array $mutated protected function addCastPropertiesToArray(array $properties, array $mutatedProperties) { foreach ($this->getCasts() as $key => $value) { - if (! array_key_exists($key, $properties) || in_array($key, $mutatedProperties)) { + if (!array_key_exists($key, $properties) || in_array($key, $mutatedProperties)) { continue; } @@ -1371,14 +1375,17 @@ protected function addCastPropertiesToArray(array $properties, array $mutatedPro // then we will serialize the date for the array. This will convert the dates // to strings based on the date format specified for these Entity models. $properties[$key] = $this->castProperty( - $key, $properties[$key] + $key, + $properties[$key] ); // If the property cast was a date or a datetime, we will serialize the date as // a string. This allows the developers to customize how dates are serialized // into an array without affecting how they are persisted into the storage. - if ($properties[$key] && - ($value === 'date' || $value === 'datetime')) { + if ( + $properties[$key] && + ($value === 'date' || $value === 'datetime') + ) { $properties[$key] = $this->serializeDate($properties[$key]); } } @@ -1403,7 +1410,7 @@ protected function getArrayableProperties() */ protected function getArrayableAppends() { - if (! count($this->appends)) { + if (!count($this->appends)) { return []; } @@ -1493,7 +1500,7 @@ protected function getArrayableItems(array $values) */ public function getProperty($key) { - if (! $key) { + if (!$key) { return; } @@ -1504,8 +1511,10 @@ public function getProperty($key) // If the property exists in the properties array or has a "get" mutator we will // get the property's value. Otherwise, we will proceed as if the developers // are asking for a relationship's value. This covers both types of values. - if (array_key_exists($key, $this->properties) || - $this->hasGetMutator($key)) { + if ( + array_key_exists($key, $this->properties) || + $this->hasGetMutator($key) + ) { return $this->getPropertyValue($key); } @@ -1547,8 +1556,10 @@ public function getPropertyValue($key) // If the property is listed as a date, we will convert it to a DateTime // instance on retrieval, which makes it quite convenient to work with // date fields without having to create a mutator for each property. - if (in_array($key, $this->getDates()) && - ! is_null($value)) { + if ( + in_array($key, $this->getDates()) && + !is_null($value) + ) { return $this->asDateTime($value); } @@ -1576,7 +1587,7 @@ protected function getPropertyFromArray($key) */ public function hasGetMutator($key) { - return method_exists($this, 'get'.Str::studly($key).'Property'); + return method_exists($this, 'get' . Str::studly($key) . 'Property'); //return method_exists($this, 'get_'.$key); } @@ -1589,7 +1600,7 @@ public function hasGetMutator($key) */ protected function mutateProperty($key, $value) { - return $this->{'get'.Str::studly($key).'Property'}($value); + return $this->{'get' . Str::studly($key) . 'Property'}($value); // return $this->{'get_'.$key}($value); } diff --git a/src/IODataClient.php b/src/IODataClient.php index 789b893..a20447c 100644 --- a/src/IODataClient.php +++ b/src/IODataClient.php @@ -13,6 +13,38 @@ interface IODataClient */ public function getAuthenticationProvider(); + /** + * Set the odata.maxpagesize value of the request. + * + * @param int $pageSize + * + * @return IODataClient + */ + public function setPageSize($pageSize); + + /** + * Gets the page size + * + * @return int + */ + public function getPageSize(); + + /** + * Set the entityKey to be found. + * + * @param mixed $entityKey + * + * @return IODataClient + */ + public function setEntityKey($entityKey); + + /** + * Gets the entity key + * + * @return mixed + */ + public function getEntityKey(); + /** * Gets the base URL for requests of the client. * @var string @@ -51,6 +83,8 @@ public function select($properties = []); public function query(); /** + * Run a GET HTTP request against the service. + * * @param $requestUri * @param array $bindings * @@ -58,6 +92,26 @@ public function query(); */ public function get($requestUri, $bindings = []); + /** + * Run a GET HTTP request against the service. + * + * @param $requestUri + * @param array $bindings + * + * @return IODataRequest + */ + public function getNextPage($requestUri, $bindings = []); + + /** + * Run a GET HTTP request against the service and return a generator + * + * @param $requestUri + * @param array $bindings + * + * @return IODataRequest + */ + public function cursor($requestUri, $bindings = []); + /** * Get the query grammar used by the connection. * diff --git a/src/ODataClient.php b/src/ODataClient.php index 7fa8567..c3b0e1b 100644 --- a/src/ODataClient.php +++ b/src/ODataClient.php @@ -9,6 +9,7 @@ use SaintSystems\OData\Query\IGrammar; use SaintSystems\OData\Query\IProcessor; use SaintSystems\OData\Query\Processor; +use Illuminate\Support\LazyCollection; class ODataClient implements IODataClient { @@ -51,6 +52,20 @@ class ODataClient implements IODataClient */ private $entityReturnType; + /** + * The page size + * + * @var int + */ + private $pageSize; + + /** + * The entityKey to be found + * + * @var mixed + */ + private $entityKey; + /** * Constructs a new ODataClient. * @param string $baseUrl The base service URL. @@ -207,10 +222,51 @@ public function query() * @return IODataRequest */ public function get($requestUri, $bindings = []) + { + list($response, $nextPage) = $this->getNextPage($requestUri, $bindings); + return $response; + } + + /** + * Run a GET HTTP request against the service. + * + * @param string $requestUri + * @param array $bindings + * @param array $skipToken + * + * @return IODataRequest + */ + public function getNextPage($requestUri, $bindings = []) { return $this->request(HttpMethod::GET, $requestUri); } + /** + * Run a GET HTTP request against the service and return a generator. + * + * @param string $requestUri + * @param array $bindings + * + * @return \Illuminate\Support\LazyCollection + */ + public function cursor($requestUri, $bindings = []) + { + return LazyCollection::make(function() use($requestUri, $bindings) { + + $nextPage = $requestUri; + + while (!is_null($nextPage)) { + list($data, $nextPage) = $this->getNextPage($nextPage, $bindings); + + if (!is_null($nextPage)) { + $nextPage = str_replace($this->baseUrl, '', $nextPage); + } + + yield from $data; + } + }); + } + /** * Run a POST request against the service. * @@ -268,13 +324,6 @@ public function request($method, $requestUri, $body = null) $request->attachBody($body); } - // TODO: find a better solution for this - /* - if ($method === 'PATCH' || $method === 'DELETE') { - $request->addHeaders(array('If-Match' => '*')); - } - */ - return $request->execute(); } @@ -331,4 +380,46 @@ public function setEntityReturnType($entityReturnType) { $this->entityReturnType = $entityReturnType; } + + /** + * Set the odata.maxpagesize value of the request. + * + * @param int $pageSize + * + * @return IODataClient + */ + public function setPageSize($pageSize) { + $this->pageSize = $pageSize; + return $this; + } + + /** + * Gets the page size + * + * @return int + */ + public function getPageSize() { + return $this->pageSize; + } + + /** + * Set the entityKey to be found. + * + * @param mixed $entityKey + * + * @return IODataClient + */ + public function setEntityKey($entityKey) { + $this->entityKey = $entityKey; + return $this; + } + + /** + * Gets the entity key + * + * @return mixed + */ + public function getEntityKey() { + return $this->entityKey; + } } diff --git a/src/ODataRequest.php b/src/ODataRequest.php index 01c58ce..f10fa21 100644 --- a/src/ODataRequest.php +++ b/src/ODataRequest.php @@ -97,6 +97,20 @@ public function __construct( } $this->timeout = 0; $this->headers = $this->getDefaultHeaders(); + $pageSize = $this->client->getPageSize(); + if (!is_null($pageSize) && is_int($pageSize)) { + $this->setPageSize($pageSize); + } + } + + /** + * Undocumented function + * + * @param [type] $pageSize + * @return void + */ + public function setPageSize($pageSize) { + $this->headers[RequestHeader::PREFER] = Constants::ODATA_MAX_PAGE_SIZE . '=' . $pageSize; } /** @@ -215,15 +229,23 @@ public function execute() $this->authenticateRequest($request); + // if (strpos($this->requestUrl, '$skiptoken') !== false) { + // echo PHP_EOL; + // echo 'Sending request: '. $this->requestUrl; + // echo PHP_EOL; + // } $result = $this->client->getHttpProvider()->send($request); + // Reset + $this->client->setEntityKey(null); + //Send back the bare response if ($this->returnsStream) { return $result; } if ($this->isAggregate()) { - return (string) $result->getBody(); + return [(string) $result->getBody(), null]; } // Wrap response in ODataResponse layer @@ -238,7 +260,7 @@ public function execute() throw new ODataException(Constants::UNABLE_TO_PARSE_RESPONSE); } - // If no return type is specified, return DynamicsResponse + // If no return type is specified, return ODataResponse $returnObj = [$response]; $returnType = is_null($this->returnType) ? Entity::class : $this->returnType; @@ -246,7 +268,9 @@ public function execute() if ($returnType) { $returnObj = $response->getResponseAsObject($returnType); } - return $returnObj; + $nextLink = $response->getNextLink(); + + return [$returnObj, $nextLink]; } /** @@ -305,13 +329,11 @@ function ($reason) { private function getDefaultHeaders() { $headers = [ - //RequestHeader::HOST => $this->client->getBaseUrl(), RequestHeader::CONTENT_TYPE => ContentType::APPLICATION_JSON, RequestHeader::ODATA_MAX_VERSION => Constants::MAX_ODATA_VERSION, RequestHeader::ODATA_VERSION => Constants::ODATA_VERSION, - RequestHeader::PREFER => Constants::ODATA_MAX_PAGE_SIZE_DEFAULT, + RequestHeader::PREFER => Constants::ODATA_MAX_PAGE_SIZE . '=' . Constants::ODATA_MAX_PAGE_SIZE_DEFAULT, RequestHeader::USER_AGENT => 'odata-sdk-php-' . Constants::SDK_VERSION, - //RequestHeader::AUTHORIZATION => 'Bearer ' . $this->accessToken ]; if (!$this->isAggregate()) { @@ -349,6 +371,11 @@ private function isAggregate() private function addHeadersToRequest(HttpRequestMessage $request) { $request->headers = array_merge($this->headers, $request->headers); + if (strpos($request->requestUri, '/$count') !== false || !is_null($this->client->getEntityKey())) { + $request->headers = array_filter($request->headers, function($key) { + return $key !== RequestHeader::PREFER; + }, ARRAY_FILTER_USE_KEY); + } } /** diff --git a/src/ODataResponse.php b/src/ODataResponse.php index e973cd7..c398c95 100644 --- a/src/ODataResponse.php +++ b/src/ODataResponse.php @@ -1,19 +1,20 @@ -request = $request; @@ -80,10 +81,10 @@ public function __construct($request, $body = null, $httpStatusCode = null, $hea } /** - * Decode the JSON response into an array - * - * @return array The decoded response - */ + * Decode the JSON response into an array + * + * @return array The decoded response + */ private function decodeBody() { $decodedBody = json_decode($this->body, true); @@ -91,8 +92,7 @@ private function decodeBody() $matches = null; preg_match('~\{(?:[^{}]|(?R))*\}~', $this->body, $matches); $decodedBody = json_decode($matches[0], true); - if ($decodedBody === null) - { + if ($decodedBody === null) { $decodedBody = array(); } } @@ -100,52 +100,52 @@ private function decodeBody() } /** - * Get the decoded body of the HTTP response - * - * @return array The decoded body - */ + * Get the decoded body of the HTTP response + * + * @return array The decoded body + */ public function getBody() { return $this->decodedBody; } /** - * Get the undecoded body of the HTTP response - * - * @return string The undecoded body - */ + * Get the undecoded body of the HTTP response + * + * @return string The undecoded body + */ public function getRawBody() { return $this->body; } /** - * Get the status of the HTTP response - * - * @return string The HTTP status - */ + * Get the status of the HTTP response + * + * @return string The HTTP status + */ public function getStatus() { return $this->httpStatusCode; } /** - * Get the headers of the response - * - * @return array The response headers - */ + * Get the headers of the response + * + * @return array The response headers + */ public function getHeaders() { return $this->headers; } /** - * Converts the response JSON object to a OData SDK object - * - * @param mixed $returnType The type to convert the object(s) to - * - * @return mixed object or array of objects of type $returnType - */ + * Converts the response JSON object to a OData SDK object + * + * @param mixed $returnType The type to convert the object(s) to + * + * @return mixed object or array of objects of type $returnType + */ public function getResponseAsObject($returnType) { $class = $returnType; @@ -164,29 +164,43 @@ public function getResponseAsObject($returnType) } /** - * Gets the skip token of a response object from OData - * - * @return string skip token, if provided - */ - public function getSkipToken() + * Gets the @odata.nextLink of a response object from OData + * + * @return string next link, if provided + */ + public function getNextLink() { if (array_key_exists(Constants::ODATA_NEXT_LINK, $this->getBody())) { $nextLink = $this->getBody()[Constants::ODATA_NEXT_LINK]; - $url = explode("?", $nextLink)[1]; - $url = explode("skiptoken=", $url); - if (count($url) > 1) { - return $url[1]; - } + return $nextLink; + } + return null; + } + + /** + * Gets the skip token of a response object from OData + * + * @return string skip token, if provided + */ + public function getSkipToken() + { + $nextLink = $this->getNextLink(); + if (is_null($nextLink)) { return null; + }; + $url = explode("?", $nextLink)[1]; + $url = explode("skiptoken=", $url); + if (count($url) > 1) { + return $url[1]; } return null; } /** - * Gets the Id of response object (if set) from OData - * - * @return mixed id if this was an insert, if provided - */ + * Gets the Id of response object (if set) from OData + * + * @return mixed id if this was an insert, if provided + */ public function getId() { if (array_key_exists(Constants::ODATA_ID, $this->getHeaders())) { diff --git a/src/Query/Builder.php b/src/Query/Builder.php index bb69e63..9fb2cd1 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Support\Arr; use Illuminate\Support\Collection; +use Illuminate\Support\LazyCollection; use SaintSystems\OData\Constants; use SaintSystems\OData\Exception\ODataQueryException; use SaintSystems\OData\IODataClient; @@ -114,6 +115,13 @@ class Builder */ public $take; + /** + * The desired page size. + * + * @var int + */ + public $pageSize; + /** * The number of records to skip. * @@ -121,6 +129,13 @@ class Builder */ public $skip; + /** + * The skiptoken. + * + * @var int + */ + public $skiptoken; + /** * All of the available clause operators. * @@ -226,7 +241,7 @@ public function from($entitySet) public function whereKey($id) { $this->entityKey = $id; - + $this->client->setEntityKey($this->entityKey); return $this; } @@ -879,6 +894,19 @@ public function skip($value) return $this; } + /** + * Set the "$skiptoken" value of the query. + * + * @param int $value + * + * @return Builder|static + */ + public function skipToken($value) + { + $this->skiptoken = $value; + return $this; + } + /** * Set the "$top" value of the query. * @@ -892,6 +920,20 @@ public function take($value) return $this; } + /** + * Set the desired pagesize of the query; + * + * @param int $value + * + * @return Builder|static + */ + public function pageSize($value) + { + $this->pageSize = $value; + $this->client->setPageSize($this->pageSize); + return $this; + } + /** * Execute the query as a "GET" request. * @@ -1027,6 +1069,20 @@ protected function runGet() ); } + /** + * Get a lazy collection for the given request. + * + * @return \Illuminate\Support\LazyCollection + */ + public function cursor() + { + return new LazyCollection(function() { + yield from $this->client->cursor( + $this->grammar->compileSelect($this), $this->getBindings() + ); + }); + } + /** * Run the query as a "GET" request against the client. * diff --git a/src/Query/Grammar.php b/src/Query/Grammar.php index e42e486..cab0fd9 100644 --- a/src/Query/Grammar.php +++ b/src/Query/Grammar.php @@ -50,6 +50,7 @@ class Grammar implements IGrammar //'search', 'orders', 'skip', + 'skiptoken', 'take', 'totalCount', ]; @@ -177,6 +178,7 @@ protected function compileQueryString(Builder $query, $queryString) || isset($query->expands) || isset($query->take) || isset($query->skip) + || isset($query->skiptoken) )) { return $queryString; } @@ -420,6 +422,19 @@ protected function compileSkip(Builder $query, $skip) return $this->appendQueryParam('$skip=') . (int) $skip; } + /** + * Compile the "$skiptoken" portions of the query. + * + * @param Builder $query + * @param int $skip + * + * @return string + */ + protected function compileSkipToken(Builder $query, $skiptoken) + { + return $this->appendQueryParam('$skiptoken=') . (int) $skiptoken; + } + /** * Compile the "$count" portions of the query. * diff --git a/tests/ODataClientTest.php b/tests/ODataClientTest.php index 2548523..045a164 100644 --- a/tests/ODataClientTest.php +++ b/tests/ODataClientTest.php @@ -3,8 +3,11 @@ namespace SaintSystems\OData\Tests; use PHPUnit\Framework\TestCase; - +use SaintSystems\OData\Entity; use SaintSystems\OData\ODataClient; +use Illuminate\Support\LazyCollection; +use SaintSystems\OData\Constants; +use SaintSystems\OData\RequestHeader; class ODataClientTest extends TestCase { @@ -12,7 +15,7 @@ class ODataClientTest extends TestCase public function setUp(): void { - $this->baseUrl = 'http://services.odata.org/V4/TripPinService'; + $this->baseUrl = 'https://services.odata.org/V4/TripPinService'; } public function testODataClientConstructor() @@ -20,7 +23,7 @@ public function testODataClientConstructor() $odataClient = new ODataClient($this->baseUrl); $this->assertNotNull($odataClient); $baseUrl = $odataClient->getBaseUrl(); - $this->assertEquals('http://services.odata.org/V4/TripPinService/', $baseUrl); + $this->assertEquals('https://services.odata.org/V4/TripPinService/', $baseUrl); } public function testODataClientEntitySetQuery() @@ -28,7 +31,6 @@ public function testODataClientEntitySetQuery() $odataClient = new ODataClient($this->baseUrl); $this->assertNotNull($odataClient); $people = $odataClient->from('People')->get(); - //dd($people); $this->assertTrue(is_array($people->toArray())); } @@ -37,7 +39,6 @@ public function testODataClientEntitySetQueryWithSelect() $odataClient = new ODataClient($this->baseUrl); $this->assertNotNull($odataClient); $people = $odataClient->select('FirstName','LastName')->from('People')->get(); - //dd($people); $this->assertTrue(is_array($people->toArray())); } @@ -46,9 +47,8 @@ public function testODataClientFromQueryWithWhere() $odataClient = new ODataClient($this->baseUrl); $this->assertNotNull($odataClient); $people = $odataClient->from('People')->where('FirstName','Russell')->get(); - // dd($people); $this->assertTrue(is_array($people->toArray())); - $this->assertTrue($people->count() == 1); + $this->assertEquals(1, $people->count()); } public function testODataClientFromQueryWithWhereOrWhere() @@ -61,7 +61,7 @@ public function testODataClientFromQueryWithWhereOrWhere() ->get(); // dd($people); $this->assertTrue(is_array($people->toArray())); - $this->assertTrue($people->count() == 2); + $this->assertEquals(2, $people->count()); } public function testODataClientFromQueryWithWhereOrWhereArrays() @@ -79,7 +79,7 @@ public function testODataClientFromQueryWithWhereOrWhereArrays() ]) ->get(); $this->assertTrue(is_array($people->toArray())); - $this->assertTrue($people->count() == 2); + $this->assertEquals(2, $people->count()); } public function testODataClientFromQueryWithWhereOrWhereArraysAndOperators() @@ -97,7 +97,7 @@ public function testODataClientFromQueryWithWhereOrWhereArraysAndOperators() ]) ->get(); $this->assertTrue(is_array($people->toArray())); - $this->assertTrue($people->count() == 2); + $this->assertEquals(2, $people->count()); } public function testODataClientFind() @@ -105,7 +105,194 @@ public function testODataClientFind() $odataClient = new ODataClient($this->baseUrl); $this->assertNotNull($odataClient); $person = $odataClient->from('People')->find('russellwhyte'); - //dd($person); - $this->assertEquals('Russell', $person->FirstName); + $this->assertEquals('russellwhyte', $person->UserName); + } + + public function testODataClientSkipToken() + { + $pageSize = 8; + $odataClient = new ODataClient($this->baseUrl, function($request) use($pageSize) { + $request->headers[RequestHeader::PREFER] = Constants::ODATA_MAX_PAGE_SIZE . '=' . $pageSize; + }); + $this->assertNotNull($odataClient); + $odataClient->setEntityReturnType(false); + $page1response = $odataClient->from('People')->get()->first(); + $page1results = collect($page1response->getResponseAsObject(Entity::class)); + $this->assertEquals($pageSize, $page1results->count()); + + $page1skiptoken = $page1response->getSkipToken(); + if ($page1skiptoken) { + $page2response = $odataClient->from('People')->skiptoken($page1skiptoken)->get()->first(); + $page2results = collect($page2response->getResponseAsObject(Entity::class)); + $page2skiptoken = $page2response->getSkipToken(); + $this->assertEquals($pageSize, $page2results->count()); + } + + $lastPageSize = 4; + if ($page2skiptoken) { + $page3response = $odataClient->from('People')->skiptoken($page2skiptoken)->get()->first(); + $page3results = collect($page3response->getResponseAsObject(Entity::class)); + $page3skiptoken = $page3response->getSkipToken(); + $this->assertEquals($lastPageSize, $page3results->count()); + $this->assertNull($page3skiptoken); + } + } + + public function testODataClientCursorBeLazyCollection() + { + $odataClient = new ODataClient($this->baseUrl); + + $pageSize = 8; + + $data = $odataClient->from('People')->pageSize($pageSize)->cursor(); + + $this->assertInstanceOf(LazyCollection::class, $data); + } + + public function testODataClientCursorCountShouldEqualTotalEntitySetCount() + { + $odataClient = new ODataClient($this->baseUrl); + + $pageSize = 8; + + $data = $odataClient->from('People')->pageSize($pageSize)->cursor(); + + $expectedCount = 20; + + $this->assertEquals($expectedCount, $data->count()); + } + + public function testODataClientCursorToArrayCountShouldEqualPageSize() + { + $odataClient = new ODataClient($this->baseUrl); + + $pageSize = 8; + + $data = $odataClient->from('People')->pageSize($pageSize)->cursor(); + + $this->assertEquals($pageSize, count($data->toArray())); + } + + public function testODataClientCursorFirstShouldReturnEntityRussellWhyte() + { + $odataClient = new ODataClient($this->baseUrl); + + $pageSize = 8; + + $data = $odataClient->from('People')->pageSize($pageSize)->cursor(); + + $first = $data->first(); + $this->assertInstanceOf(Entity::class, $first); + $this->assertEquals('russellwhyte', $first->UserName); + } + + public function testODataClientCursorLastShouldReturnEntityKristaKemp() + { + $odataClient = new ODataClient($this->baseUrl); + + $pageSize = 8; + + $data = $odataClient->from('People')->pageSize($pageSize)->cursor(); + + $last = $data->last(); + $this->assertInstanceOf(Entity::class, $last); + $this->assertEquals('kristakemp', $last->UserName); + } + + public function testODataClientCursorSkip1FirstShouldReturnEntityScottKetchum() + { + $odataClient = new ODataClient($this->baseUrl); + + $pageSize = 8; + + $data = $odataClient->from('People')->pageSize($pageSize)->cursor(); + + $second = $data->skip(1)->first(); + $this->assertInstanceOf(Entity::class, $second); + $this->assertEquals('scottketchum', $second->UserName); + } + + public function testODataClientCursorSkip4FirstShouldReturnEntityWillieAshmore() + { + $odataClient = new ODataClient($this->baseUrl); + + $pageSize = 8; + + $data = $odataClient->from('People')->pageSize($pageSize)->cursor(); + + $fifth = $data->skip(4)->first(); + $this->assertInstanceOf(Entity::class, $fifth); + $this->assertEquals('willieashmore', $fifth->UserName); + } + + public function testODataClientCursorSkip7FirstShouldReturnEntityKeithPinckney() + { + $odataClient = new ODataClient($this->baseUrl); + + $pageSize = 8; + + $data = $odataClient->from('People')->pageSize($pageSize)->cursor(); + + $eighth = $data->skip(7)->first(); + $this->assertInstanceOf(Entity::class, $eighth); + $this->assertEquals('keithpinckney', $eighth->UserName); + } + + public function testODataClientCursorSkip8FirstShouldReturnEntityMarshallGaray() + { + $odataClient = new ODataClient($this->baseUrl); + + $pageSize = 8; + + $data = $odataClient->from('People')->pageSize($pageSize)->cursor(); + + $ninth = $data->skip(8)->first(); + $this->assertInstanceOf(Entity::class, $ninth); + $this->assertEquals('marshallgaray', $ninth->UserName); + } + + public function testODataClientCursorSkip16FirstShouldReturnEntitySandyOsbord() + { + $odataClient = new ODataClient($this->baseUrl); + + $pageSize = 8; + + $data = $odataClient->from('People')->pageSize($pageSize)->cursor(); + + $seventeenth = $data->skip(16)->first(); + $this->assertInstanceOf(Entity::class, $seventeenth); + $this->assertEquals('sandyosborn', $seventeenth->UserName); + } + + public function testODataClientCursorSkip16LastPageShouldBe4Records() + { + $odataClient = new ODataClient($this->baseUrl); + + $pageSize = 8; + + $data = $odataClient->from('People')->pageSize($pageSize)->cursor(); + + $lastPage = $data->skip(16); + $lastPageSize = 4; + $this->assertEquals($lastPageSize, count($lastPage->toArray())); + } + + public function testODataClientCursorIteratingShouldReturnAll20Entities() + { + $odataClient = new ODataClient($this->baseUrl); + + $pageSize = 8; + + $data = $odataClient->from('People')->pageSize($pageSize)->cursor(); + + $expectedCount = 20; + $counter = 0; + + $data->each(function ($person) use(&$counter) { + $counter++; + $this->assertInstanceOf(Entity::class, $person); + }); + + $this->assertEquals($expectedCount, $counter); } } diff --git a/tests/Query/BuilderTest.php b/tests/Query/BuilderTest.php index 09a18a7..2282820 100644 --- a/tests/Query/BuilderTest.php +++ b/tests/Query/BuilderTest.php @@ -113,13 +113,13 @@ public function testEntitySetCount() $entitySet = 'People'; - //$expected = 55; + $expected = 20; $actual = $builder->from($entitySet)->count(); $this->assertTrue(is_numeric($actual)); $this->assertTrue($actual > 0); - //$this->assertEquals($expected, $actual); + $this->assertEquals($expected, $actual); } // public function testEntitySetCountWithWhere()