Git Product home page Git Product logo

lodata's Introduction

Lodata - The OData v4.01 Producer for Laravel

GitHub Workflow Status OpenAPI Validator Packagist Version Packagist Downloads License Code Climate maintainability Code Climate coverage

Lodata is an implementation of the OData v4.01 Producer protocol, designed for use with the Laravel framework.

See the documentation here!

OData (Open Data Protocol) is an ISO/IEC approved, OASIS standard that defines a set of best practices for building and consuming RESTful APIs.

OData helps you focus on your business logic while building RESTful APIs without having to worry about the various approaches to define request and response headers, status codes, HTTP methods, URL conventions, media types, payload formats, query options, etc. OData also provides guidance for tracking changes, defining functions/actions for reusable procedures, and sending asynchronous/batch requests.

OData RESTful APIs are easy to consume. The OData metadata, a machine-readable description of the data model of the APIs, enables the creation of powerful generic client proxies and tools. The metadata is available in OData-specific XML and JSON formats, as well as an OpenAPI v3 document.

There are many tools and techniques for exposing APIs from Laravel and there are some specific use cases where Lodata could be a great fit for your application:

  • Developing single page applications and mobile applications with OData-supporting enterprise UI frameworks such as Sencha ExtJS, DevExtreme, Kendo UI and Syncfusion.
  • Making live connections to business intelligence tools such as Excel, PowerBI, and Tableau, avoiding clunky CSX/XLSX exports.
  • Publishing an out-of-the-box discoverable OpenAPI document for tools like Postman to help third parties interact with your application.
  • Developing microservices in Laravel. With all OData services having the same request syntax, as your team develops many services you can guarantee API consistency.
  • Create real simple integrations with enterprise applications from SAP, SalesForce and Microsoft. Present forms, tabular data and search interfaces in these applications without writing a single line of code.

You can construct OData requests using any HTTP client, but there are also many developer-friendly OData libraries for different programming languages.

Now go check out the five-minute getting started guide!

Support

Flat3 now provides commercial support for Lodata. If you need help integrating Lodata into your application, want to build a Lodata-powered service or need new features then get in touch.

lodata's People

Contributors

27pchrisl avatar coderkoala avatar dependabot[bot] avatar holdyourwaffle avatar merouanekhalili avatar mgerzabek avatar remo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

lodata's Issues

No Eager Loading ?

Hi,

When I use $expand, I have over 200 queries because eager loading is not used.

Could you please update package or tell me how to enable eager loading ?

Thanks !

how to get GUID into my odata

Hi guys, this pkg from Chris is quite neat and trying to explore every angle. My demo is using mysql and I require the odata to have a GUID which I understand can't be generated into mysql db.
Is there a work around this to get GUID for the id in the schema?
Can the Type be set dynamically?

Eloquent driver pagination

Eloquent driver pagination ($skip) stops working due to changes in commit: c1d6883

src/Drivers/EloquentEntitySet.php
In the query() method $builder->getModels() is changed with $builder->lazy(). It seems that new Laravel lazy() method cannot paginate and the $skip parameter is ignored. You always get the first $top (ex. 20) rows.

From the Laravel docs
Once again, if you plan to update the retrieved records while iterating over them, it is best to use the lazyById method instead. This method will automatically paginate the results based on the record's primary key.

Case sensitive column name is not supported

Hi,

I have a PostgresSQL database with case sensitive column names, when I try a query discovery I got the below exception :

ErrorException

Undefined array key "MY_COLUMN_NAME"

at C:\Me\www\data-api\vendor\flat3\lodata\src\Drivers\SQL\SQLSchema.php:45
41▕ }
42▕
43▕ var_dump($columns);
44▕
➜ 45▕ $column = $columns[$index->getColumns()[0]];
46▕ $key = $this->columnToDeclaredProperty($column);
47▕
48▕ if ($column->getAutoincrement()) {
49▕ $key->addAnnotation(new Computed());

1 vendor\flat3\lodata\src\Drivers\SQL\SQLSchema.php:45
Illuminate\Foundation\Bootstrap\HandleExceptions::handleError()

2 app\Providers\AppServiceProvider.php:29
Flat3\Lodata\Drivers\SQLEntitySet::discoverProperties()

Problem with $filter and name with accentuation in return

Hi,

When trying to apply $filter in a request where the name of a person has accentuation it's generated an error:

{"@context":"http://localhost:8050/odata/$metadata#persons(idperson,name)","value":[{"idperson":1138,"name":
TypeError: Flat3\Lodata\Helper\JSON::encode(): Return value must be of type string, bool returned in file C:\Projects\oData\vendor\flat3\lodata\src\Helper\JSON.php on line 33

#0 C:\Projects\oData\vendor\flat3\lodata\src\Controller\Transaction.php(1035): Flat3\Lodata\Helper\JSON::encode()
#1 C:\Projects\oData\vendor\flat3\lodata\src\Primitive.php(129): Flat3\Lodata\Controller\Transaction->sendJson()
#2 C:\Projects\oData\vendor\flat3\lodata\src\ComplexValue.php(344): Flat3\Lodata\Primitive->emitJson()
#3 C:\Projects\oData\vendor\flat3\lodata\src\EntitySet.php(221): Flat3\Lodata\ComplexValue->emitJson()
#4 C:\Projects\oData\vendor\flat3\lodata\src\EntitySet.php(281): Flat3\Lodata\EntitySet->emitJson()
#5 C:\Projects\oData\vendor\symfony\http-foundation\StreamedResponse.php(93): Flat3\Lodata\EntitySet->Flat3\Lodata\{closure}()
#6 C:\Projects\oData\vendor\flat3\lodata\src\Controller\Response.php(96): Symfony\Component\HttpFoundation\StreamedResponse->sendContent()
#7 C:\Projects\oData\vendor\flat3\lodata\src\Controller\Response.php(144): Flat3\Lodata\Controller\Response->sendContentStreamed()
#8 C:\Projects\oData\vendor\flat3\lodata\src\Helper\Symfony\Response6.php(17): Flat3\Lodata\Controller\Response->_sendContent()
#9 C:\Projects\oData\vendor\symfony\http-foundation\Response.php(373): Flat3\Lodata\Helper\Symfony\Response6->sendContent()
#10 C:\Projects\oData\public\index.php(53): Symfony\Component\HttpFoundation\Response->send()
#11 {main}

Request
http://localhost:8050/odata/persons?$select=idperson,name&top=2&filter=contains(name, 'Vitor')&skip=1

Although, if I do the request directly like http://localhost:8050/odata/persons/1138 its works without problem.
Thank you for your time !

setBoundParameter doesn't support string

In the readme we've got examples like this:

Lodata::add((new class('identity') extends Operation implements FunctionInterface {
    public function invoke(String_ $code): String_
    {
      return $code;
    }
})->setBoundParameter('code');

But this doesn't work for me, the parameter PipeInterface or null:

lodata/src/Operation.php

Lines 224 to 239 in f7c2760

/**
* Set the bound parameter on an instance of this operation
* @param PipeInterface|null $parameter Binding parameter
* @return $this
*/
public function setBoundParameter(?PipeInterface $parameter): self
{
$this->assertTransaction();
if ($parameter instanceof PropertyValue) {
$parameter = $parameter->getValue();
}
$this->boundParameter = $parameter;
return $this;
}

Store discovery data result to prevent mapping each request

I've hit a small speed issue when using lodata with a database containing >20 tables with alot of MySQL Keys. When running it adds a flat ~8s to each request, coming from doing all discover functionality in the Service Provider, which is quite an annoyance when you are fetching data from a table that has just 10 rows.

Given this issue I was wondering if it is possible to make it so that lodata has the ability to store the result of the discovery on file and also the ability to read this file again. This so that it doesn't have to do the discovery each and every request.

I was able to get this functionality working by myself by quickly adding a setter & getter for the ObjectArray $model variable in Flat3\Lodata\Model (and adding it to the facade). And then doing the following in my in ServiceProvider:

// LodataModelServiceProvider

if (Storage::exists("lodata.serialized")) {
    Lodata::setModel(unserialize(Storage::get("lodata.serialized")));
} else {

    // Execute all model discovery here...

    $serialized = serialize(Lodata::getModel());
    Storage::put("lodata.serialized", $serialized);
}

(The problem with this code is that it doesn't discover again once the data is set, but just as an example)

My question is if it is possible to have this functionality natively supported in Lodata. Even the functionality to get and set the ObjectArray $model would be enough for me, and then having the responsibility of storing, reading and serialization on the project itself.

Thanks in advance!

Other OData REST APIs as EntitySet drivers?

I am trying to build microservices using Laravel and OData, and I found your library. I noticed you mentioned EntitySet drivers could basically anything that implements the right traits. I am assuming a possible approach to building inter connected APIs is referencing other OData services as EntitiesSets and define relationships between EntityTypes on different servers, but I guess I would have to build my own "driver" for this?

Thanks in advance

How to implement Lodata without Service Provider?

Hello,

I wanted to ask what is the best way to use lodata if you don't use a global service provider? So e.g.: then inside a controller, using the add/edit/delete methods manually, but e.g.: accessing lodata for the datagrid?

Otherwise I have yes 2 different endpoints, but would like to use only one endpoint in that case. E.g.: lodata should have all users under /backend/api/Users, where I would then define more methods. Thus I would like to use lodata directly in a UserController, without a global ServiceProvider.

What would be the right approach for this?

setAlternativeKey

thanks for the pkg. Can I get help on how and where to setup the alternative key in my laravel app.
Also how do you paginate big data request

Nested $expand are not working

Context

class Person {

  #[LodataRelationship]
    public function link()
    {
        return $this->hasOne(Link::class, 'id', 'link_id' );
    }

  #[LodataRelationship]
    public function tags(){
        return $this->hasMany(Tag::class);
    }
}

class Tag {
}

class Link {
  #[LodataRelationship]
    public function sublink()
    {
        return $this->hasOne(SubLink::class, 'id', 'sublink_id' );
    }
}

class Sublink {
}

Db

Table person{id, link_id}
Table tags{id}
Table link{id}
Table sublink{id}
Table person_tag{person_id,tag_id}

Request
http://localhost:8080/odata/Person?expand=link($expand=sublink)

Result
The result is the same with this request http://localhost:8080/odata/Person?expand=link the nested $expand is not used.

{
    {
  "@context": "http://localhost:8080/odata/$metadata#Gparent01s(link())",
  "value": [
    {
      "id": 1,
      "glink01_fk": 1,
      "cproperty01": "sed earum repellendus enim mollitia sit odit incidunt numquam a",
      "nproperty02": 318,
      "bproperty03": false,
      "dproperty04": "1988-04-24T01:47:09+00:00",
      "created_at": "2022-05-03T12:46:19+00:00",
      "updated_at": "2022-05-03T12:46:19+00:00",
      "link": {
        "id": 1,
        "sublink_id": 1,
        "link": "quis eum autem laboriosam qui cupiditate quod mollitia nihil repellendus",
        "values": "voluptas sed explicabo a at cum repellat sed repellendus eos",
        "created_at": "2022-05-03T12:46:19+00:00",
        "updated_at": "2022-05-03T12:46:19+00:00"
      }
    }
}

Expected result
The result should contains the Sublink entity when present in the relationship as expected (with the correct context) : https://docs.oasis-open.org/odata/odata/v4.01/os/part1-protocol/odata-v4.01-os-part1-protocol.html#sec_ExpandOptions

{
    {
  "@context": "http://localhost:8080/odata/$metadata#Gparent01s(link(Sublink()))"
  "value": [
    {
      "id": 1,
      "glink01_fk": 1,
      "cproperty01": "sed earum repellendus enim mollitia sit odit incidunt numquam a",
      "nproperty02": 318,
      "bproperty03": false,
      "dproperty04": "1988-04-24T01:47:09+00:00",
      "created_at": "2022-05-03T12:46:19+00:00",
      "updated_at": "2022-05-03T12:46:19+00:00",
      "link": {
        "id": 1,
        "sublink_id": 1,
        "link": "quis eum autem laboriosam qui cupiditate quod mollitia nihil repellendus",
        "values": "voluptas sed explicabo a at cum repellat sed repellendus eos",
        "created_at": "2022-05-03T12:46:19+00:00",
        "updated_at": "2022-05-03T12:46:19+00:00",
        "sublink" : {
           "id": 1,
           "sublink": "quis eum autem laboriosam qui cupiditate quod mollitia nihil repellendus".
           "prop1": 1231,
           "created_at": "2022-05-20T14:47:24+00:00",
           "updated_at": "2022-05-20T15:47:24+00:00",
         }
      }
    }
}

Version
"flat3/lodata": "^5.11"

$metadata query returns 500 error

Querying http://127.0.0.1:8080/odata/$metadata returns a 500 error.

This is a simple mysql database table with 6 columns. One column, with a datatype or timestamp, uses CURRENT_TIMESTAMP as the default expression.

Field                 Type         Null KEY  Default
id                    int(11)      NO		
remote_id             bigint(20)   NO   PRI	
name                  varchar(100) NO		
status                varchar(20)  NO		
date_added            timestamp    NO        CURRENT_TIMESTAMP
lastModifiedDateTime  timestamp    YES		

The provider code is:

$entityType = new EntityType('mytable');
$entitySet = (new \Flat3\Lodata\Drivers\SQLEntitySet('mytable', $entityType))
    ->setTable('mytable')
    ->discoverProperties();
\Lodata::add($entitySet);

The log output is:

[2022-06-07 18:50:43] local.ERROR: DateTimeImmutable::__construct(): Failed to parse time string (CURRENT_TIMESTAMP) at position 0 (C): The timezone could not be found in the database {"exception":"[object] (Carbon\\Exceptions\\InvalidFormatException(code: 0): DateTimeImmutable::__construct(): Failed to parse time string (CURRENT_TIMESTAMP) at position 0 (C): The timezone could not be found in the database at /odata-laravel/vendor/nesbot/carbon/src/Carbon/Traits/Creator.php:89)
[stacktrace]
#0 /odata-laravel/vendor/flat3/lodata/src/Type/DateTimeOffset.php(45): Carbon\\CarbonImmutable->__construct()
#1 /odata-laravel/vendor/flat3/lodata/src/Primitive.php(57): Flat3\\Lodata\\Type\\DateTimeOffset->set()
#2 /odata-laravel/vendor/flat3/lodata/src/PrimitiveType.php(53): Flat3\\Lodata\\Primitive->__construct()
#3 /odata-laravel/vendor/flat3/lodata/src/Property.php(303): Flat3\\Lodata\\PrimitiveType->instance()
#4 /odata-laravel/vendor/flat3/lodata/src/PathSegment/Metadata/XML.php(161): Flat3\\Lodata\\Property->computeDefaultValue()
#5 /odata-laravel/vendor/flat3/lodata/src/PathSegment/Metadata/XML.php(350): Flat3\\Lodata\\PathSegment\\Metadata\\XML->emitStream()
#6 /odata-laravel/vendor/symfony/http-foundation/StreamedResponse.php(109): Flat3\\Lodata\\PathSegment\\Metadata\\XML->Flat3\\Lodata\\PathSegment\\Metadata\\{closure}()
#7 /odata-laravel/vendor/flat3/lodata/src/Controller/Response.php(96): Symfony\\Component\\HttpFoundation\\StreamedResponse->sendContent()
#8 /odata-laravel/vendor/flat3/lodata/src/Controller/Response.php(144): Flat3\\Lodata\\Controller\\Response->sendContentStreamed()
#9 /odata-laravel/vendor/flat3/lodata/src/Helper/Symfony/Response5.php(17): Flat3\\Lodata\\Controller\\Response->_sendContent()
#10 /odata-laravel/vendor/symfony/http-foundation/Response.php(394): Flat3\\Lodata\\Helper\\Symfony\\Response5->sendContent()
#11 /odata-laravel/public/index.php(53): Symfony\\Component\\HttpFoundation\\Response->send()
#12 /odata-laravel/server.php(21): require_once('/...')
#13 {main}

$select should only have effect on primary entity

Context

class Person {
  #[LodataRelationship]
    public function tags(){
        return $this->belongsToMany(Tag::class);
    }
}

class Tag {
}

Db

Table person{id}
Table tags{id}
Table person_tag{person_id,tag_id}

Request
http://localhost:8080/odata/Person?$select=id$expand=tags

Result

[
 {
    id 1,
    tags: [
      {id: 1},
      {id: 2}
    ]
 },
 {
    id 2,
    tags: [
      {id: 3},
      {id: 4}
    ]
 }
]

Expected result

[
 {
    id 1,
    tags: [
      {id: 1, prop1: '', prop2: ''},
      {id: 2, prop1: '', prop2: ''}
    ]
 },
 {
    id 2,
    tags: [
      {id: 3, prop1: '', prop2: ''},
      {id: 4, prop1: '', prop2: ''}
    ]
 }
]

Only with this request : http://localhost:8080/odata/Person?$select=id$expand=tags($select=id) we should have the actual result.

Proposal
Iv'e made some test and it seems that with this commit things are working correctly for simple cases : Angelinsky7@985118e but because i don't have a correct knowledge of out work it's difficult to see if it's the correct way of resolving this issue.

Version
"flat3/lodata": "^5.11"

$filter navigation entity targets the main entity

Context

class Person {

  #[LodataRelationship]
    public function link()
    {
        return $this->belongsTo(Link::class, 'link_id', 'id' );
    }

  #[LodataRelationship]
    public function tags(){
        return $this->belongsToMany(Tag::class);
    }
}

class Tag {
}

class Link {
}

Db

Table person{id, link_id}
Table tags{id}
Table link{id}
Table person_tag{person_id,tag_id}

Request
http://localhost:8080/odata/Person?$filter=link/id eq 1

Result
The result is the same with this request http://localhost:8080/odata/Person?$filter=id eq 1 the request filter only the id of the main entity not the id of the navigation entity. (we can see that this information is lost function generateWhere() of trait SQLWhere.
If we try to filter a property of the navigation link we have an error because the property does not exit. (For example, if you $filter on link/prop01 and person don't have a prop01 property in the database there will be an error :

{
    "@context": "http://localhost:8080/odata/$metadata#person(link())",
    "value": [OData-error: {
            "code": "expression_parser_error",
            "message": "Encountered an invalid symbol at: link/>p<rop01 eq 3"",
            "target": null,
            "details": [],
            "innererror": {}
        }

Expected result
We should be able to filter entities with navigation link as in https://docs.oasis-open.org/odata/odata/v4.01/os/part1-protocol/odata-v4.01-os-part1-protocol.html#sec_BuiltinFilterOperations with the first example :
Address/City eq 'Redmond'
and have a list of all person that have a link with the target property filtered.

Version
"flat3/lodata": "^5.11"

Syntax error: 7 ERROR: syntax error at or near "`

Flat3\Lodata\Exception\Protocol\InternalServerErrorException {#2343
  #httpCode: 500
  #odataCode: "query_error"
  #message: """
    The executed query returned an error: SQLSTATE[42601]: Syntax error: 7 ERROR:  syntax error at or near "`"
    LINE 1: SELECT passengers.`id` AS id, passengers.`number` AS number,...
                              ^
    """

Currently having this error when creating a PostgreSQL View from real table.

Lodata boot:

    $type = new EntityType('passenger');

    $type->addDeclaredProperty($key, Type::string());
    
    $property = $type->getProperty($key);
    
    $type->setKey($property);
    
    $set = (new SQLEntitySet('passengers', $type))
        ->setTable('passengers')
        ->discoverProperties();
        
    Lodata::add($set);

Overall simple tables with integers and strings works fine, but when JSON, relationships are in the table - throws errors which are pretty much hard to understand why.

dynamic columns in select and orderby

I'm trying to reproduce some of our hacky stuff we did during the last few months. I'm fully aware that some of it won't win us a price for elegant code, but business applications sometimes have ugly requirements.


While I do like to offload stuff to the database, sometimes we have to inject things with the help of some PHP code. I was able to add a "virtual" column by using this:

protected $appends = ['random_sort'];

public function getRandomSortAttribute()
{
    return 123;
}

This was not immedatiely visible, but that I could easily fix by adding the last line in this snippet:

\Lodata::discoverEloquentModel(\App\Models\Product::class)->discoverRelationship('descriptions');

$productType = \Lodata::getEntityType('Product');
$productType->addProperty((new DeclaredProperty('random_sort', Type::decimal())));

Great!


Step 2, sort by a virtual column.

/odata/Products?$orderby=random_sort

No luck, tries to do an SQL sort. I attempted to add it to the SQL query by adding some code to the product model:

protected static function boot()
{
    parent::boot();

    static::addGlobalScope('random_sort', function (Builder $builder) {
        $builder->select('*')->addSelect(\DB::raw('rand() AS random_sort'));
    });
}

No luck, column value doesn't show up. It does when I'm using tinker to fetch a record from the database though.

We have a surprising number of situations where we want to sort by a value that isn't found in a column. Not long ago we calculated the distance to the current location by using this https://stackoverflow.com/questions/35467774/haversine-and-laravel

Not sure how I would be able to replicate this using lodata, but maybe it's another case (in addition to this #49 (comment)) where an event would allow me to inject some column before the query is executed? An event before and after a command is executed would probably help a lot to do ugly things ;-)

Support for Eloquent hidden attributes in discoverProperties

When discovering Eloquent models it would be really useful to allow for some of the underlying table columns to be suppressed by respecting the "hidden" property of the Model. I very rarely want to expose every column of my tables.
In the SQLSchema trait I think we just need something along the lines of this (after checking we are in an EloquentEntitySet and $this->model is set)
$blacklist = array_merge( $blacklist, $this->getModel()->getHidden());

HasOne and belongsTo should have the same meaning

Context
If we use relationship we should be able to use hasOne and belongsTo and have the same result.

class Person {

  #[LodataRelationship]
    public function link()
    {
        //Those two statement should have the same behavior....
        return $this->belongsTo(Link::class, 'link_id', 'id' );
        return $this->hasOne(Link::class, ,'id', 'link_id' );
    }
}

class Link {
}

Db

Table person{id, link_id}
Table link{id}

Result
When getting constraints information about a navigation property, if the navigation property is not describe as HasOne or HasOneOrMany there is no information about the relationship.

if ($r instanceof HasOne || $r instanceof HasOneOrMany) {
$localProperty = $this->getType()->getProperty($r->getLocalKeyName());
$foreignProperty = $right->getType()->getProperty($r->getForeignKeyName());
if (!$localProperty || !$foreignProperty) {
throw new ConfigurationException(
'missing_properties',
'The properties referenced for the relationship could not be found on the models'
);
}
$rc = new ReferentialConstraint($localProperty, $foreignProperty);
$nav->addConstraint($rc);
}
if ($r instanceof BelongsTo || $r instanceof HasOneThrough || $r instanceof HasOne) {
$nav->setCollection(false);
}

Expected result
We should be able to get information about the relationship either if we use HasOne or BelongsTo as stated in https://laravel.com/docs/9.x/eloquent-relationships#one-to-one-defining-the-inverse-of-the-relationship

Version
"flat3/lodata": "^5.11"

Lodata not respecting nullable property when inserting data

When I send a POST request for creating a model that has an attribute which is marked as non-nullable it still proceeds and ends up trying to insert the data in the database with without a value for the non-nullable field, which ends up giving an SQL error. Below is an example of the request. Perhaps I need to specify on a secondary place that an attribute is required, but I wasn't able to find how to do this if so.

I am using version 4.0.1 of Lodata.

A smaller secondary remark that I have, is that in my instance I would prefer when a InternalServerErrorException would be thrown by the Transaction that it would actually throw the exception instead of creating a response of it directly. This way I also can prevent from the given error messages to be too crude (and perhaps too informative) for the end user.

image

Thanks in advance for the reply!

Pass original exception when uncaptured exception occurs on Transaction execute

Whenever a Transaction gets executed, and the source throws an uncaptured exception, it gets pretty tough figuring out the original exception because it does not contain the "previous" exception, only the message.

I think it would be a decent addition that the InternalServerErrorException that gets thrown at Flat3\Lodata\Controller\Transaction.php:1271 gets the original exception passed along so that the error handling can check the details of this error if needed.

support for deep inserts

I'm trying to create an entity include a child entity for these two models:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{   
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'product_name',
        'price',
    ];

    public function descriptions()
    {
        return $this->hasMany(ProductDescription::class);
    }

}

and

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class ProductDescription extends Model
{   
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'product_id',
        'language',
        'description',
    ];

    public function product()
    {
        return $this->belongsTo(Product::class);
    }
}

My payload looks like this:

{
    "product_name" : "Product A",  
    "price" : 200,
    "descriptions": [
        {
            "language":"de",
            "description": "Produkt A"
        }
    ] 
}

It doesn't seem to care about the descriptions and only creates the top level product record.

According to the specification they call this deep insert. I haven't found anything in the code about that. Is this something which isn't supported? Any plans to support it?

This is the section I'm talking about:
https://docs.oasis-open.org/odata/odata/v4.01/os/part1-protocol/odata-v4.01-os-part1-protocol.html#sec_CreateRelatedEntitiesWhenCreatinganE

Composer issue with Laravel 9

Laravel 9 has some changes with illuminate/events requiring version 9. But the current 5.2 version of Lodata still requires version 8. I'm not certain if this brings any breaking changes, but as for now Lodata does not work with Laravel 9.

  Problem 1
    - illuminate/events[v8.0.0, ..., v8.11.2] require php ^7.3 -> your php version (8.1.1) does not satisfy that requirement.
    - Only one of these can be installed: illuminate/events[v8.0.0, ..., 8.x-dev], laravel/framework[v9.0.0-beta.1, ..., 9.x-dev]. laravel/framework replaces illuminate/events and thus cannot coexist with it.
    - flat3/lodata[v5.2.0, ..., 5.x-dev] require illuminate/events ^8.0 -> satisfiable by illuminate/events[v8.0.0, ..., 8.x-dev].
    - Root composer.json requires flat3/lodata ^5.2 -> satisfiable by flat3/lodata[v5.2.0, 5.x-dev].
    - Root composer.json requires laravel/framework ^9.0 -> satisfiable by laravel/framework[v9.0.0-beta.1, ..., 9.x-dev].

How to Implement Navigation Link?

i'm trying to generate response like below
each entity can have a selfLink and an navigationLink

{
  "@iot.count": 1,
  "value": [
    {
      "@iot.id": 1,
      "@iot.selfLink": "https://toronto-bike-snapshot.sensorup.com/v1.0/Locations(1)",
      "name": "7061:Bloor St / Brunswick Ave",
      "[email protected]": "https://toronto-bike-snapshot.sensorup.com/v1.0/Locations(1)/Things",
    }
  ]
}

my laravel code

Lodata::discoverEloquentModel(Location::class);
Lodata::getEntitySet('Locations')->discoverRelationship('Things');

here's my questions

  • how can i show NavigationBinding (e.g. "Things") by default and display as navigation link
  • how to use "@" in property name

i tried many different ways (like generatedProperty) but nothing work 😭
plz help

Reference:
https://toronto-bike-snapshot.sensorup.com/v1.0/Locations

Operations and discovering

Hi,

I'm in trouble with discovering and operations. I'm on the last version.

Here is what I have :

// First class
class User extends Model {
	#[LodataRelationship]
	public function orders() {
		return $this->hasMany(Order::class);
	}
	
	#[LodataFunction(return: "array")]
	public function function1(string $param){
		...
	}
}

// Second class
class Order extends Model {
	#[LodataRelationship]
	public function user() {
		return $this->belongsTo(User::class);
	}
	
	#[LodataFunction(return: "array")]
	public function function2(string $param){
		...
	}
}

// Discovering
\Lodata::discover(\App\Models\User::class);

The function1 is working, but the function2 is not (No route handler was able to process this request)

If I change discovering for :

\Lodata::discover(\App\Models\Order::class);
\Lodata::discover(\App\Models\User::class);

Then the function2 is working but not function1.

Any idea?
Thanks!

primary key type string despite using numeric id

I just discovered lodata, currently playing around to see if it's an alternative for our own implementation. It looks pretty great at first glance, thanks for sharing it!

My demo project has a simple model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{   
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'product_name',
        'price',
    ];
}

I can query it by using /odata/Products, great. I then wanted to query a single record, I tried /odata/Products(1), no luck, it fails with:

{"code":"invalid_identifier_value","message":"The type of the provided identifier value was not valid for this entity type","details":">1<"}

I did some digging and it seems like it's detected as a string. I tried to define keyType, but not luck. Same for $casts:

    protected $casts = [
        'id' => 'integer',
    ];

As soon as I've changed it to a string, it worked /odata/Products(id='1') or /odata/Products('1').

Is there something I missed that allows me to define the data type of properties?

Problem when load XML metadata

Hi @27pchrisl

When I tried to load the XML metadata http://localhost:8000/odata/$metadata its generated an error because some fields in my database has the default value int.

TypeError: SimpleXMLElement::addAttribute(): Argument #2 ($value) must be of type string, int given in file C:\Projects\odata\vendor\flat3\lodata\src\PathSegment\Metadata\XML.php on line 161

#0 C:\Projects\odata\vendor\flat3\lodata\src\PathSegment\Metadata\XML.php(161): SimpleXMLElement->addAttribute()
#1 C:\Projects\odata\vendor\flat3\lodata\src\PathSegment\Metadata\XML.php(350): Flat3\Lodata\PathSegment\Metadata\XML->emitStream()
#2 C:\Projects\odata\vendor\symfony\http-foundation\StreamedResponse.php(93): Flat3\Lodata\PathSegment\Metadata\XML->Flat3\Lodata\PathSegment\Metadata\{closure}()
#3 C:\Projects\odata\vendor\flat3\lodata\src\Controller\Response.php(96): Symfony\Component\HttpFoundation\StreamedResponse->sendContent()
#4 C:\Projects\odata\vendor\flat3\lodata\src\Controller\Response.php(144): Flat3\Lodata\Controller\Response->sendContentStreamed()
#5 C:\Projects\odata\vendor\flat3\lodata\src\Helper\Symfony\Response6.php(17): Flat3\Lodata\Controller\Response->_sendContent()
#6 C:\Projects\odata\vendor\symfony\http-foundation\Response.php(373): Flat3\Lodata\Helper\Symfony\Response6->sendContent()
#7 C:\Projects\odata\public\index.php(53): Symfony\Component\HttpFoundation\Response->send()
#8 {main}

But when editing the file src/PathSegment/Metadata/XML.php function emitStream line 161 and adding (string) $property->computeDefaultValue()->toJson() works very vell.

                if ($property->hasStaticDefaultValue()) {
                    $entityTypeProperty->addAttribute('DefaultValue', (string) $property->computeDefaultValue()->toJson());
                }

Thanks !

Return only allowed data

Hi!

Has a way to intercept a response or even add a subquery in a request?
Because in some cases, it's necessary to restrict data return identifying the user in jwt.

Thanks !

Memory problem in class relationship

Hi,

First of all, thank you for your time and for this amazing package.
However, when I use #[LodataRelationship] between two classes a memory issue is generated.

Error

PHP Fatal error:  Allowed memory size of 536870912 bytes exhausted (tried to allocate 32768 bytes) in C:\Projects\oData\vendor\laravel\framework\src\Illuminate\Filesystem\Filesystem.php on line 77
PHP Fatal error:  Allowed memory size of 536870912 bytes exhausted (tried to allocate 32768 bytes) in C:\Projects\oData\vendor\symfony\error-handler\Error\FatalError.php on line 1

Classes

namespace App\Models\PAD;

use App\Models\ACD\Student;
use Flat3\Lodata\Attributes\LodataIdentifier;
use Flat3\Lodata\Attributes\LodataRelationship;
use Illuminate\Database\Eloquent\Model;

#[LodataIdentifier('persons')]
class Person extends Model
{
    protected $table = 'person';
    protected $primaryKey = 'idperson';
    public $timestamps = false;

    protected $fillable = [
        'idperson',
        'firstname',
	'lastname',
	'email'
    ];
	
    #[LodataRelationship]
    public function student()
    {
        return $this->hasMany(Student::class, 'idperson', 'idperson');
    }
}


namespace App\Models\ACD;

use App\Models\PAD\Person;
use Flat3\Lodata\Attributes\LodataIdentifier;
use Flat3\Lodata\Attributes\LodataRelationship;
use Illuminate\Database\Eloquent\Model;

#[LodataIdentifier('students')]
class Student extends Model
{
    protected $table = 'student';
    protected $primaryKey = 'idstudent';
    public $timestamps = false;

    protected $fillable = [
        'idstudent',
        'idperson',
	'idschool',
	'username',
	'password'
    ];
	
    #[LodataRelationship]
    public function person()
    {
        return $this->belongsTo(Person::class, 'idperson', 'idperson');
    }
}

Request
http://localhost:8050/odata

Version
"flat3/lodata": "^5.17"

If I comment the relationship person() in Student class the error isn't generated.
Thanks !

Pulling data from SQL views

Would it be possible to extend the package to process SQL views, not only the tables? We are using postgres and there is no possibility to set identifier field for view.

Loading Expanded Model from Expanded Model

I am not sure if this is possible within that library, but can i use an Expanded Query on and Expanded Objekt?

For Example:

Clients -> Locations -> Countries

Each Client as Multiple Locations, and Each Location has Multiple Country Objects. When ill know try with this query:

http://localhost/api/Clients/query?$expand=locations($expand=country)

Then it seems the nested Expand is not working (but as i understand the oData Library this should work)

Is there a way to handle this within the lodata library?

Thanks in advance

Doctrine\DBAL\Driver\PDO\Exception could not find driver

Hi Chris, hope all is well. I seem to have this error message when using the pkg. I installed the app on a linux server and run composer update which comes up with the above error message. Can you please advise as to the solution if you have encountered something similar
composer.json require

php : ^7.3 | ^8.0
doctrine/dbal: ^2.1
flar3/lodata: 61.11
framework : ^8.4

Default/forced filter?

Is there a way to enforce a sort of "default" filter? Basically, there is data in the table that should never show on the API. Think of it like there is a Status field and data should only ever show when Status is Active and it should never show otherwise.

lodata with existing CRUD API

I have my CRUD APIs up and running but struggling to find the correct URI for odata end point.

odata/api/assets yields below message :

image

I have my Assets GET route defined in api.php as :

Route::get('/assets', [AssetController::class, 'index']);

What should I do next ?

$count for navigation collection is not correct

Context

class Person {
  #[LodataRelationship]
    public function tags(){
        return $this->belongsToMany(Tag::class);
    }
}

class Tag {
}

Db

Table person{id}
Table tags{id}
Table person_tag{person_id,tag_id}

Request
http://localhost:8080/odata/Person/1/tags/$count

Result
The total of all tags in the db: The same as if the request was : http://localhost:8080/odata/tags/$count

SELECT COUNT(*) FROM tags

Expected result
The query should be the count of all tags of the selected person. Not all the tags.

SELECT COUNT(*) FROM tags t JOIN person_tag pt ON pt.tag_id = t.id WHERE pt.person_id = ?;

But of course the laravel builder can already do that by it self using the queryBuilder.

Version
"flat3/lodata": "^5.11"

Sort by a property in an expanded relationship

Hi,

Can you help me please find a solution to the following problem:

With this request http://localhost/api/odata/Customers?$top=10&$expand=city($select=id,name) I have the following result:

{ "@context": "http://localhost/api/odata/$metadata#Customers(name,county(),city())", "value": [ { "name": "Sallie Maggio DVM", "county": { "id": 25, "name": "Braila", "code": "BR" }, "city": { "id": 3214, "county_id": 25, "name": "Chioibasesti" } }, { "name": "Winona Gottlieb", "county": { "id": 12, "name": "Olt", "code": "OT" }, "city": { "id": 8547, "county_id": 12, "name": "Rescuta" } } ], "@nextLink": "http://localhost/api/odata/Customers?%24expand=county%2Ccity&select=name&top=2&skip=2" }

When I try to sort the result by a child (expand property), I get the error:

The orderby parameter specified properties (city.name) that did not exist

Request: http://localhost/api/odata/Customers?$top=10&$expand=city($select=id,name)&$orderby=city.name%20desc

What can I do?

Thank you very much for your help!
Have a nice day!

DeclaredProperty on relationship

Hi, I have some troubles with this case :

I have :

\Lodata::discover(\App\Models\Offer::class);
\Lodata::discover(\App\Models\Product::class);

$entity = \Lodata::getEntityType('Offer');
$entity->addDeclaredProperty( 'name', Type::string() );
$entity->addDeclaredProperty( 'description', Type::string() );

$entity = \Lodata::getEntityType('Product');
$entity->addDeclaredProperty( 'name', Type::string() );
$entity->addDeclaredProperty( 'description', Type::string() );

I can see name and description on offer, but not on product. But if i comment out the first line, it works. I think it's because I have a relationship between Offer and Product

Offer.php
#[LodataRelationship]
public function product() {
    return $this->belongsTo(Product::class);
}

How can I make my case work ?

Thank you

Array to string conversion on Collection toEtag

Hi, I'm sorry to came up with another issue 😕

I have a DeclaredProperty of type Collection. When I do odata/Offers all is good. But when I do odata/Offers(1) I have the following error Array to string conversion. This exception is thrown in file Primitive.php on line 98 (return null === $value ? null : (string) $value;). This exception makes sense because $value is an array.

When I comment out these lines in ComplexValue.php on (382-384), all is good :

if ($value instanceof ETagInterface) {
    $input[$property->getName()] = $value->toEtag();
}

Is this a bug or I'm doing something wrong ?

Thanks again

$select inside $expand only respects single attribute

I'm just posting this as I play around with lodata, there's no real use case for now, but before I start delving into the code and attempt to fix things on my own I'd like to know if lodata could work for us. For now just issues if you don't mind.

I've got two models in my demo project:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{   
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'product_name',
        'price',
    ];

    public function descriptions()
    {
        return $this->hasMany(ProductDescription::class);
    }

}

and

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class ProductDescription extends Model
{   
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'product_id',
        'language',
        'description',
    ];

    public function product()
    {
        return $this->belongsTo(Product::class);
    }
}

I expose it using this:

\Lodata::discoverEloquentModel(\App\Models\ProductDescription::class);
\Lodata::discoverEloquentModel(\App\Models\Product::class)->discoverRelationship('descriptions');

I wanted to fetch a single product with all the descriptions. It works fine using this /odata/Products(id=1)?$select=id,product_name&$expand=descriptions, but I thought the product_id inside the descriptions array is superfluous and tried to remove it using this /odata/Products(id=1)?$select=id,product_name&$expand=descriptions($select=language,description)

The result of this is:

{
   "@context":"http://localhost:2015/odata/$metadata#Products(id,product_name,descriptions(language))/$entity",
   "id":1,
   "product_name":"Mickey",
   "descriptions":[
      {
         "language":"de"
      },
      {
         "language":"en"
      }
   ]
}

The description is missing. No matter what attribute I define, it only seems to include the first one.

Error when the columns name contains a dot

When I define an SqlEntitySet with a table having dots in the column name, I have the following error calling the API:

{"error":{"code":"invalid_name","message":"The provided name was invalid: Account__r.ExtId__c","target":null,"details":[],"innererror":{}}}

If I change the column name and don't use dots, it works.
The code is the following:

$segmentsType = new \Flat3\Lodata\EntityType('Object');
        $segmentsSet = (new \Flat3\Lodata\Drivers\SQLEntitySet('Object', $segmentsType))
            -> setTable('table')
            -> discoverProperties();
        \Lodata::add($segmentsSet);

I'm using MariaDb 10.5.12 and PHP 7.4.28
Thanks in advance

EloquentEntitySet::Discover does not respect a Model's connection preference.

Great work. I've noticed that with 3.x and 4.x that if I do discovery on an eloquent model that uses a different connection than the default, it will fail to discover appropriately.

The use case here is leveraging both MySQL and Postgres databases to power the same application, and setting "connection" on the model to the appropriate connection. See: https://laravel.com/docs/8.x/eloquent#database-connections

The fix seems to be setting the connection name in: \Flat3\Lodata\Drivers\EloquentEntitySet::__construct

$this->setConnectionName($this->getModel()->getConnectionName() ?: DB::getDefaultConnection());

best practices regarding code structure

I'm playing around with lodata again, everything works very well and a lot of the OData specification has been implemented which is great.

One thing I'm struggling with, is the whole code structure.

Operations

If I extend a class from Operation I basically build a global class, but most of the time an operation is specific to a model. If I have a model called Person, I would want the operation greet to be part of that model/class and not other model.

In pseudo code I'd probably want something like this:

class Person extends BaseRepository {
   model = PersonModel::class

   greet() {
      ...
   }
}

And then register the whole class, including the public methods which would turn into operations. That way I can nicely group my code which would make it easier for new people on my project to find the relevant pieces.

I'm aware that I can bind an entity, but this again looks global: https://lodata.io/modelling/operations.html#binding-entities

Overriding standard behaviour

This seems to be a use-case which I need a lot. If I take my pseudo code from above, I might want to turn it into something like this:

class Person extends BaseRepository {
   model = PersonModel::class

   greet() {
      ...
   }

   onSave() {
      fullName = first_name + middle_name + last_name
   }
}

In other words:

  • While I don't want to have classes with hundreds of lines, I also don't want to have a single file per method.
  • I'd like to keep things together from a business object point of view.

Maybe I could create a magic object register method that scans a bunch of annotations and register the entity as well as certain operations, but I wonder what others are doing to structure their lodata code.

Operator functions not working when lodata is configured for readonly

For a project I'm using the RepositoryInterface from lodata to use as ground for my Operator functions. This project I am using this on is used as a read-only project, but has some operator functions to pass some data to an external source. But when trying to create this I noticed that operator functions always throw the "This service is read-only" message when executed (when readonly=true in the config, works when readonly=false).

Now I was wondering if this was indeed intended this way or not, because I'd prefer to only expose a few operator endpoints instead of the entire Create/Update/Delete function stack.

Thanks in advance for the help!

lodata is only odata 4.01 but devextreme version 4.0 only

It seems currently I cannot use with laravel lodata the devextreme because, the devextreme is 4.0 compatible, and the lodata is 4.01. (response contains @odata.count and in the 4.01 the "odata." was removed. Should be a configuration switch to keep compatibility mode 4.0 . So currently the devextreme odatastore not working.

Odata Schema

Hi Chris

How do I pull the schema to get the collections names and data types

regards
K

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.