From 5fe6a21307a5f856afb67e1f30263e298625d770 Mon Sep 17 00:00:00 2001 From: Chris Lloyd Date: Sat, 31 Oct 2020 17:19:12 +0000 Subject: [PATCH] WIP --- README.md | 32 +++++++++++++++++-- src/Controller/Transaction.php | 9 ++++++ src/Entity.php | 10 ++++-- src/EntitySet.php | 10 ++++-- src/Helper/PropertyValue.php | 9 +++--- src/Interfaces/EmitInterface.php | 2 +- src/PathSegment/Count.php | 3 +- src/PathSegment/Metadata.php | 3 +- src/PathSegment/Service.php | 3 +- src/PathSegment/Value.php | 3 +- src/Primitive.php | 6 ++-- tests/Unit/Eloquent/EloquentTest.php | 23 ++++++++++++- ...st__test_expand_belongsto_property__1.json | 2 +- ...d_belongsto_property_full_metadata__1.json | 2 +- ..._property_select_existent_property__1.json | 14 ++++++++ ...roperty_select_nonexistent_property__1.yml | 3 ++ ...test_expand_hasone_entity_property__1.json | 2 +- ...tTest__test_expand_hasone_property__1.json | 2 +- ...tTest__test_expand_hasone_property__2.json | 2 +- 19 files changed, 115 insertions(+), 25 deletions(-) create mode 100644 tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_belongsto_property_select_existent_property__1.json create mode 100644 tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_belongsto_property_select_nonexistent_property__1.yml diff --git a/README.md b/README.md index b4ea22eb4..ca557d1a0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,15 @@ # Flat3 OData 4.01 Producer for Laravel +

+Build Status +Latest Stable Version +License +

+ ## What is OData? (from the OData spec) -The OData Protocol is an application-level protocol for interacting with data via RESTful interfaces. The protocol supports the description of data models and the editing and querying of data according to those models. It provides facilities for: +The OData Protocol is an application-level protocol for interacting with data via RESTful interfaces. The protocol supports the +description of data models and the editing and querying of data according to those models. It provides facilities for: - Metadata: a machine-readable description of the data model exposed by a particular service. - Data: sets of data entities and the relationships between them. @@ -11,7 +18,8 @@ The OData Protocol is an application-level protocol for interacting with data vi - Operations: invoking custom logic - Vocabularies: attaching custom semantics -The OData Protocol is different from other REST-based web service approaches in that it provides a uniform way to describe both the data and the data model. This improves semantic interoperability between systems and allows an ecosystem to emerge. +The OData Protocol is different from other REST-based web service approaches in that it provides a uniform way to describe +both the data and the data model. This improves semantic interoperability between systems and allows an ecosystem to emerge. Towards that end, the OData Protocol follows these design principles: - Prefer mechanisms that work on a variety of data sources. In particular, do not assume a relational data model. - Extensibility is important. Services should be able to support extended functionality without breaking clients unaware of those extensions. @@ -19,6 +27,26 @@ Towards that end, the OData Protocol follows these design principles: - OData should build incrementally. A very basic, compliant service should be easy to build, with additional work necessary only to support additional capabilities. - Keep it simple. Address the common cases and provide extensibility where necessary. +## Getting started + +First require lodata inside your existing Laravel application: + +``` +composer require flat3/lodata +``` + +Now start your app, the OData API endpoint will now be available at: http://127.0.0.1:8000/odata (or whichever port your application normally runs on). + +If you access that URL you'll see an "unauthorized" message. By default the endpoint is wrapped in HTTP Basic Authentication. +You can either provide basic auth credentials of an existing user, or you can temporarily disable authentication by including this in your +Laravel .env file: + +``` +LODATA_DISABLE_AUTH=1 +``` + +Access the URL again, you'll see the Service Document. The Metadata Document will also be available at: http://127.0.0.1:8000/odata/$metadata + ## Specification * https://docs.oasis-open.org/odata/odata/v4.01/os/part1-protocol/odata-v4.01-os-part1-protocol.html diff --git a/src/Controller/Transaction.php b/src/Controller/Transaction.php index 8afbc4950..d4e409372 100644 --- a/src/Controller/Transaction.php +++ b/src/Controller/Transaction.php @@ -716,6 +716,15 @@ public function getId(): string return $this->id; } + public function replaceQueryParams(Transaction $incomingTransaction): self + { + foreach (['select'] as $param) { + $this->$param = $incomingTransaction->$param; + } + + return $this; + } + public function execute(): EmitInterface { $pathSegments = $this->getPathSegments(); diff --git a/src/Entity.php b/src/Entity.php index cee99af7c..f629428cc 100644 --- a/src/Entity.php +++ b/src/Entity.php @@ -322,12 +322,16 @@ public function getResourceUrl(Transaction $transaction): string ); } - public function response(Transaction $transaction): Response + public function response(Transaction $transaction, ?ContextInterface $context = null): Response { - $transaction = $this->transaction ?: $transaction; + if ($this->transaction) { + $transaction = $this->transaction->replaceQueryParams($transaction); + } + + $context = $context ?: $this; $this->metadata = [ - 'context' => $this->getContextUrl($transaction), + 'context' => $context->getContextUrl($transaction), ]; return $transaction->getResponse()->setCallback(function () use ($transaction) { diff --git a/src/EntitySet.php b/src/EntitySet.php index 7acb293e3..d7ec95bf7 100644 --- a/src/EntitySet.php +++ b/src/EntitySet.php @@ -245,14 +245,18 @@ public function emit(Transaction $transaction): void $transaction->outputJsonArrayEnd(); } - public function response(Transaction $transaction): Response + public function response(Transaction $transaction, ?ContextInterface $context = null): Response { - $transaction = $this->transaction ?: $transaction; + if ($this->transaction) { + $transaction = $this->transaction->replaceQueryParams($transaction); + } + + $context = $context ?: $this; $setCount = $this->count(); $metadata = [ - 'context' => $this->getContextUrl($transaction), + 'context' => $context->getContextUrl($transaction), ]; $count = $transaction->getCount(); diff --git a/src/Helper/PropertyValue.php b/src/Helper/PropertyValue.php index 8cf38f183..e5262fe35 100644 --- a/src/Helper/PropertyValue.php +++ b/src/Helper/PropertyValue.php @@ -190,20 +190,21 @@ public function emit(Transaction $transaction): void ); } - public function response(Transaction $transaction): Response + public function response(Transaction $transaction, ?ContextInterface $context = null): Response { $value = $this->value; + $context = $context ?: $this; if ($value instanceof Primitive && null === $value->get() || $value === null) { throw new NoContentException('null_value'); } - if ($value instanceof Entity) { - return $value->response($transaction); + if ($value instanceof Entity || $value instanceof EntitySet) { + return $value->response($transaction, $this); } $metadata = [ - 'context' => $this->getContextUrl($transaction), + 'context' => $context->getContextUrl($transaction), ]; $metadata = $transaction->getMetadata()->filter($metadata); diff --git a/src/Interfaces/EmitInterface.php b/src/Interfaces/EmitInterface.php index 93dcf375c..1ee97239a 100644 --- a/src/Interfaces/EmitInterface.php +++ b/src/Interfaces/EmitInterface.php @@ -9,5 +9,5 @@ interface EmitInterface { public function emit(Transaction $transaction): void; - public function response(Transaction $transaction): Response; + public function response(Transaction $transaction, ?ContextInterface $context = null): Response; } \ No newline at end of file diff --git a/src/PathSegment/Count.php b/src/PathSegment/Count.php index ba62d6b5b..78cf76b80 100644 --- a/src/PathSegment/Count.php +++ b/src/PathSegment/Count.php @@ -7,6 +7,7 @@ use Flat3\Lodata\Controller\Transaction; use Flat3\Lodata\Exception\Internal\PathNotHandledException; use Flat3\Lodata\Exception\Protocol\BadRequestException; +use Flat3\Lodata\Interfaces\ContextInterface; use Flat3\Lodata\Interfaces\EmitInterface; use Flat3\Lodata\Interfaces\PipeInterface; @@ -25,7 +26,7 @@ public function emit(Transaction $transaction): void $transaction->outputRaw($this->countable->count()); } - public function response(Transaction $transaction): Response + public function response(Transaction $transaction, ?ContextInterface $context = null): Response { return $transaction->getResponse()->setCallback(function () use ($transaction) { $this->emit($transaction); diff --git a/src/PathSegment/Metadata.php b/src/PathSegment/Metadata.php index ea954ca5d..dc65364d2 100644 --- a/src/PathSegment/Metadata.php +++ b/src/PathSegment/Metadata.php @@ -10,6 +10,7 @@ use Flat3\Lodata\Exception\Protocol\BadRequestException; use Flat3\Lodata\Facades\Lodata; use Flat3\Lodata\Helper\Constants; +use Flat3\Lodata\Interfaces\ContextInterface; use Flat3\Lodata\Interfaces\EmitInterface; use Flat3\Lodata\Interfaces\PipeInterface; use Flat3\Lodata\NavigationBinding; @@ -240,7 +241,7 @@ public function emit(Transaction $transaction): void $transaction->outputRaw($root->asXML()); } - public function response(Transaction $transaction): Response + public function response(Transaction $transaction, ?ContextInterface $context = null): Response { $transaction->ensureMethod(Request::METHOD_GET); diff --git a/src/PathSegment/Service.php b/src/PathSegment/Service.php index 607a0e359..c72d9d332 100644 --- a/src/PathSegment/Service.php +++ b/src/PathSegment/Service.php @@ -5,13 +5,14 @@ use Flat3\Lodata\Controller\Response; use Flat3\Lodata\Controller\Transaction; use Flat3\Lodata\Facades\Lodata; +use Flat3\Lodata\Interfaces\ContextInterface; use Flat3\Lodata\Interfaces\EmitInterface; use Flat3\Lodata\Interfaces\ServiceInterface; use Illuminate\Http\Request; class Service implements EmitInterface { - public function response(Transaction $transaction): Response + public function response(Transaction $transaction, ?ContextInterface $context = null): Response { $transaction->ensureMethod(Request::METHOD_GET); diff --git a/src/PathSegment/Value.php b/src/PathSegment/Value.php index 757b5c15a..8277becf0 100644 --- a/src/PathSegment/Value.php +++ b/src/PathSegment/Value.php @@ -8,6 +8,7 @@ use Flat3\Lodata\Exception\Protocol\BadRequestException; use Flat3\Lodata\Exception\Protocol\NoContentException; use Flat3\Lodata\Helper\PropertyValue; +use Flat3\Lodata\Interfaces\ContextInterface; use Flat3\Lodata\Interfaces\EmitInterface; use Flat3\Lodata\Interfaces\PipeInterface; use Flat3\Lodata\Primitive; @@ -46,7 +47,7 @@ public static function pipe( return new static($value); } - public function response(Transaction $transaction): Response + public function response(Transaction $transaction, ?ContextInterface $context = null): Response { if (null === $this->primitive->get()) { throw new NoContentException('null_value'); diff --git a/src/Primitive.php b/src/Primitive.php index 69c161925..c9359dfd2 100644 --- a/src/Primitive.php +++ b/src/Primitive.php @@ -152,14 +152,16 @@ public function emit(Transaction $transaction): void $transaction->outputJsonValue($this); } - public function response(Transaction $transaction): Response + public function response(Transaction $transaction, ?ContextInterface $context = null): Response { if (null === $this->get()) { throw new NoContentException('null_value'); } + $context = $context ?: $this; + $metadata = [ - 'context' => $this->getContextUrl($transaction), + 'context' => $context->getContextUrl($transaction), ]; $metadata = $transaction->getMetadata()->filter($metadata); diff --git a/tests/Unit/Eloquent/EloquentTest.php b/tests/Unit/Eloquent/EloquentTest.php index d601e2ffa..207f7c8a2 100644 --- a/tests/Unit/Eloquent/EloquentTest.php +++ b/tests/Unit/Eloquent/EloquentTest.php @@ -11,7 +11,6 @@ use Flat3\Lodata\Tests\Request; use Flat3\Lodata\Tests\TestCase; use Flat3\Lodata\Transaction\Metadata; -use Illuminate\Database\Eloquent\Relations\HasOneThrough; class EloquentTest extends TestCase { @@ -356,6 +355,28 @@ public function test_expand_belongsto_property() ); } + public function test_expand_belongsto_property_select_nonexistent_property() + { + $this->withFlightData(); + + $this->assertBadRequest( + Request::factory() + ->path('/Flights(1)/passengers') + ->query('$select', 'naame') + ); + } + + public function test_expand_belongsto_property_select_existent_property() + { + $this->withFlightData(); + + $this->assertJsonResponse( + Request::factory() + ->path('/Flights(1)/passengers') + ->query('$select', 'name') + ); + } + public function test_expand_belongsto_property_full_metadata() { $this->withFlightData(); diff --git a/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_belongsto_property__1.json b/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_belongsto_property__1.json index 6b731f694..68bef1e24 100644 --- a/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_belongsto_property__1.json +++ b/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_belongsto_property__1.json @@ -1,5 +1,5 @@ { - "@context": "http://localhost/odata/$metadata#Flights/$entity", + "@context": "http://localhost/odata/$metadata#Passengers(1)/flight", "id": 1, "origin": "lhr", "destination": "lax", diff --git a/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_belongsto_property_full_metadata__1.json b/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_belongsto_property_full_metadata__1.json index 7dc6c3af9..4e56e4a87 100644 --- a/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_belongsto_property_full_metadata__1.json +++ b/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_belongsto_property_full_metadata__1.json @@ -1,5 +1,5 @@ { - "@context": "http://localhost/odata/$metadata#Flights/$entity", + "@context": "http://localhost/odata/$metadata#Passengers(1)/flight", "@type": "#com.example.odata.Flight", "@id": "http://localhost/odata/Flights(1)", "@readLink": "http://localhost/odata/Flights(1)", diff --git a/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_belongsto_property_select_existent_property__1.json b/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_belongsto_property_select_existent_property__1.json new file mode 100644 index 000000000..848014e45 --- /dev/null +++ b/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_belongsto_property_select_existent_property__1.json @@ -0,0 +1,14 @@ +{ + "@context": "http://localhost/odata/$metadata#Flights(1)/passengers", + "value": [ + { + "name": "Anne Arbor" + }, + { + "name": "Bob Barry" + }, + { + "name": "Charlie Carrot" + } + ] +} diff --git a/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_belongsto_property_select_nonexistent_property__1.yml b/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_belongsto_property_select_nonexistent_property__1.yml new file mode 100644 index 000000000..3ea6319ca --- /dev/null +++ b/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_belongsto_property_select_nonexistent_property__1.yml @@ -0,0 +1,3 @@ +httpCode: 400 +odataCode: property_does_not_exist +message: 'The requested property "naame" does not exist on this entity type' diff --git a/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_hasone_entity_property__1.json b/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_hasone_entity_property__1.json index f32976723..5d50b6b1e 100644 --- a/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_hasone_entity_property__1.json +++ b/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_hasone_entity_property__1.json @@ -1,5 +1,5 @@ { - "@context": "http://localhost/odata/$metadata#Countries/$entity", + "@context": "http://localhost/odata/$metadata#Airports(1)/country", "id": 1, "name": "en" } diff --git a/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_hasone_property__1.json b/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_hasone_property__1.json index e55538203..5716fe0e4 100644 --- a/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_hasone_property__1.json +++ b/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_hasone_property__1.json @@ -1,5 +1,5 @@ { - "@context": "http://localhost/odata/$metadata#Airports/$entity", + "@context": "http://localhost/odata/$metadata#Flights(1)/originAirport", "id": 1, "name": "Heathrow", "code": "lhr", diff --git a/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_hasone_property__2.json b/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_hasone_property__2.json index b10533217..82f0da118 100644 --- a/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_hasone_property__2.json +++ b/tests/Unit/Eloquent/__snapshots__/EloquentTest__test_expand_hasone_property__2.json @@ -1,5 +1,5 @@ { - "@context": "http://localhost/odata/$metadata#Airports/$entity", + "@context": "http://localhost/odata/$metadata#Flights(3)/destinationAirport", "id": 2, "name": "Los Angeles", "code": "lax",