Spring Data Rest PATCH add to embedded list - spring

I'm having difficulty adding to an embedded list using Spring Data Rest and a PATCH request. I'm using MongoDB so no JPA joins (ManyToOne etc) here, just a plain old regular embedded List of child type.
My beans look like this:
class Parent {
String name;
List<Child> children;
}
class Child {
String name;
}
My request looks like this:
curl -d '{"children": [ {"name": "bob"} ] }' -H "Content-Type: application/json" -X PATCH http://localhost:8080/api/parent/123
The result of this is that all the child elements are replaced with the new one from the request, e.g.
old: [ 'tom', 'sally' ]
request: [ 'bob' ]
expected result: [ 'tom', 'sally', 'bob']
actual result: [ 'bob' ]
I've stepped through the Spring code (DomainObjectReader) and it just doesn't seem to handle my scenario but surely this is a really simple use case, any ideas? Am I missing something obvious?
Thanks!

The problem is tha you try to edit the parent object, instead of the relationship collection.
You need to create a child:
POST /api/child {...}
which returns a url to the newly created child (either in the location header or as a self link in the response - based on your SDR settings)
Then add this child to the parent's children collection:
PATCH /api/parent/123/children
Content-Type:text/uri-list
body: ***the URI of the child***

Related

API Platform GraphQL creates generically typed IterableConnection with entity specific connection for related entities

Summary
When creating two entities with a relation, and at the same time using a custom DataProvider (implementing CollectionDataProviderInterface and RestrictedDataProviderInterface), the GraphQL schema generates a generic IterableConnection instead of a Connection specific for the relation.
Works: Book (example entity provided by api-platform which uses Doctrine ORM) has the following GraphQL field:
reviews(
first: Int
last: Int
before: String
after: String
): ReviewConnection # Connection which is specific to Reviews
Does not work: Agent (customer entity using a custom DataProvider) has the following GraphQL field:
dept(
first: Int
last: Int
before: String
after: String
): IterableConnection # <-- Connection which is generic, expected DepartmentConnection
Detailed description
The example entities provided in the documentation under Getting Started with API Platform: Hypermedia and GraphQL API, Admin and Progressive Web App: Book and Review, works correctly out of the box.
The Book entity will have a GraphQL field called reviews which is of type ReviewConnection.
So for instance a Book could be queried like this:
{
book(id: "/books/1") {
reviews { # type: ReviewConnection
edges { # type: [ReviewEdge]
node { # type: Review
id
body
publicationDate
book {
description
}
}
}
}
}
}
However for the custom entities, using a custom DataProvider, the connections are typed as a generic IterableConnection. This makes it impossible to query the related entity using GraphQL, since the entity behind the connection is not known. The rest API still works though, so from the looks of it the entities and DataProviders are indeed correct.
The corresponding query for an Agent would look like this:
{
agent(id: "/agents/1") {
dept { # type: IterableConnection
edges { # type: [IterableEdge] (expected type: [DepartmentEdge])
node # type: Iterable (expected type: Department)
}
}
}
}
Also when executing this GraphQL query, the following exception is thrown:
Argument 1 passed to ApiPlatform\\Core\\GraphQl\\Serializer\\SerializerContextBuilder::replaceIdKeys()
must be of the type array, bool given, called in
/srv/api/vendor/api-platform/core/src/GraphQl/Serializer/SerializerContextBuilder.php on line 72
Basically it boils down to that in SerializerContextBuilder.php on line 72 the variable $fields will look like this for a Book:
$fields = [
'reviews' => [
'edges' => [
'node' => [
'id' => true
]
]
]
];
While for an Agent it will lack the last array level with the 'id':
$fields = [
'dept' => [
'edges' => [
'node' => true
]
]
];
However, I figured the Exception is not the root cause in that sense - because the error is already at the point when the schema is generated.
I cannot figure out where the problem lies, if it's on the entity, DataProvider or somewhere else.
The Book entity, which works correctly, looks exactly as in the documentation.
The relevant parts are basically:
/**
* #var Review[] Available reviews for this book.
*
* #ORM\OneToMany(targetEntity="Review", mappedBy="book", cascade={"persist", "remove"})
*/
public $reviews;
Here it seems to be enough to type the property as #var Review[], and the GraphQL field called review will automatically be of type ReviewConnection, and thus containing the schema of the Review entity.
The custom entity Agent however, which doesn't work as expected, looks basically the same, but without the ORM annotations:
/**
* #var Department[]
*/
public $dept;
It works to query both agent and agents, and also department and departments in GraphQL. This means that the DataProviders works, and that the schema generated for a single entity is correct, but not the connection to related entites.
Any suggestions grately appreciated!

Updating property and resource link in single PUT query with Spring Data Rest

OK so let's start self referencing object, something like this:
#Data
#Entity
public class FamilyNode {
#Id
#GeneratedValue
private long id;
private boolean orphan;
#ManyToOne
private FamilyNode parent;
}
And a standard repository rest resource like this:
#RepositoryRestResource(collectionResourceRel = "familynodes", path = "familynodes")
public interface FamilyNodeRepository extends CrudRepository<FamilyNode, Long> {
}
Now, let's assume some parent objects which I want to link with already exist with ID=1 and ID=2, each of which were created with a POST to /api/familynodes which looked like this:
{
"orphan": true,
}
If I attempt to create a new client (ID=3) with something like this using a POST request to /api/familynodes, it will work fine with the linked resource updating fine in the DB:
{
"orphan": false,
"parent": "/api/familynodes/1"
}
However, if I attempt to do a PUT with the following body to /api/familynodes/3, the parent property seems to silently do nothing and the database is not updated to reflect the new association:
{
"orphan": false,
"parent": "/api/familynodes/2"
}
Similarly (and this is the use case that I'm getting at), a PUT like this will only update the orphan property but will leave the parent untouched:
{
"orphan": true,
"parent": null
}
So you now have a record which claims to be an orphan, but still has a parent. Of course you could do subsequent REST requests to the resource URI directly but I'm trying to make rest operations atomic so that it's impossible for any single rest query to create invalid state. So now I'm struggling with how do that with what seems like a simple use case without getting into writing my own controller to handle it - am I missing a mechanism here within the realm of spring data rest?
This is the expected behaviour for PUT requests in Spring Data Rest 2.5.7 and above wherein a PUT request does not update the resource links, only the main attributes.
As detailed here by Oliver Gierke:
If we consider URIs for association fields in the payload to update those associations, the question comes up about what's supposed to happen if no URI is specified. With the current behavior, linked associations are simply not a part of the payload as they only reside in the _links block. We have two options in this scenario: wiping the associations that are not handed, which breaks the "PUT what you GET" approach. Only wiping the ones that are supplied using null would sort of blur the "you PUT the entire state of the resource".
You may use a PATCH instead of PUT to achieve the desired result in your case

Spring Data Rest Mongo - how to create a DBRef using an id instead of a URI?

I have the following entity, that references another entity.
class Foo {
String id;
String name supplierName;
**#DBRef** TemplateSchema templateSchema;
...
}
I want to be able to use the following JSON (or similar) to create a new entity.
{
"supplierName": "Stormkind",
"templateSchema": "572878138b749120341e6cbf"
}
...but it looks like Spring forces you to use a URI like this:
{
"supplierName": "Stormkind",
"templateSchema": "/template-schema/572878138b749120341e6cbf"
}
Is there a way to create the DBRef by posting an ID instead of a URI?
Thanks!
In REST, the only form of ID's that exist are URIs (hence the name Unique Resource Identifier). Something like 572878138b749120341e6cbf does not identify a resource, /template-schema/572878138b749120341e6cbf does.
On the HTTP level, entities do not exist, only resources identified by URIs. That's why Spring Data REST expects you to use URIs as identifiers.

Spring Data Mongo nested id won't work

Does Spring Data treats mongo nested "id" attributes differently? I explain my problem: I have collection matches with the following structure
"teams": [
{
"id" : "5601",
"name" : "FC Basel"
},
... // more
]
When I want to retrieve all the matches which has team id 5601 I execute the following query
db.matches.find({ "teams.id" : "5601"})
Which works perfectly and returns some objects.
When I make a method
public List<MatchMongo> findByTeams_id(String id);
on my MatchRepository interface I get 0 results back while there are.
Logs shows
Created query Query: { "teams.id" : "5601"}, Fields: null, Sort: null
find using query: { "teams.id" : "5601"} fields: null for class: class
MatchMongo in collection: matches
So the query he makes seems to be the right one... :S
Trying with other fields (referee.name for ex.) works.
I even tried with the #Query annotation, but can't get it to work
Is there another solution? Is this a bug or am I doing something wrong?
Oh found the solution:
MatchMongo had List<TeamMongo> teams; on whereI had
#Id
private String id;
#Field(value = "id")
private String teamIdAttr;
So the method should be called
public List<MatchMongo> findByTeams_teamIdAttr(String id);
Never thought the method name should reflect objects attributes instead of collection structure
Thanks #martin-baumgartner your comment helped to solve this :)

How can I write a nested serializer that behaves differently when writing or reading?

I am looking for a way to create a nested serializer which will behave differently when writing or reading. In my specific case, I have some models that look like this:
class Frame(models.Model):
frame_stack = models.IntegerField()
frame_reach = models.IntegerField()
# ...
class CustomBicycle(models.Model):
frame = models.ForeignKey(Frame)
# ...
When the CustomBicycle model is serialized, I want it to look like this:
{
"id": 1,
"frame": {
"id": 1,
"frame_stack": 123,
"frame_reach": 234
}
}
Let's say I want to submit some JSON to be de-serialized in order to update the above CustomBicycle instance. In that case, I'd like to submit the following JSON in order to update its Frame to the one with id = 2:
{
"id": 1,
"frame": {
"id": 2,
"frame_stack": 345,
"frame_reach": 456
}
}
The serializer I need would look at the "frame.id" property in the JSON and then update the CustomBicycle instance to point to Frame #2. It would ignore all other properties under the "frame" key in the JSON (but it should allow those properties to be there).
The reason I want this is that I have something analogous to model classes in the Javascript of my application. I can easily serialize these Javascript model instances into JSON that looks like that which is above. I don't want to have to program in extra logic to serialize the model data in the ways that would be necessary to work with a flat JSON API, as is required by the current version of Django Rest Framework.
Any ideas?
Here's my own kinda hacky solution to this one:
class NestedForeignKeyAssignmentMixin(object):
def field_from_native(self, data, files, field_name, into):
if self.read_only:
return
value = data.get(field_name)
id = value.get('id') if value else None
if not id:
if self.required:
raise ValidationError(self.error_messages['required'])
into[field_name] = None
return
model = self.Meta.model
try:
into[field_name] = model._default_manager.get(id=id)
except model.DoesNotExist:
raise ValidationError('{name} with id `{id}` does not exist'.format(
name=model.__name__,
id=id,
))
class FrameSerializer(NestedForeignKeyAssignmentMixin, serializers.ModelSerializer):
# ...
class Meta:
model = Frame
class CustomBicycleSerializer(serializers.ModelSerializer):
frame = FrameSerializer()
# ...

Resources