8000 [Bugfix] Ensure duplicates are not added to a has-many relation · AxonDivisionDev/laravel-json-api@869aca9 · GitHub
[go: up one dir, main page]

Skip to content

Commit 869aca9

Browse files
committed
[Bugfix] Ensure duplicates are not added to a has-many relation
The spec states that duplicates must not be added, but the default Laravel behaviour is to add duplicates. This commit changes the has-many relation so that it filters out duplicates before adding them on a `POST` request. Closes cloudcreativity#222
1 parent f3c858a commit 869aca9

File tree

3 files changed

+67
-1
lines changed

3 files changed

+67
-1
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ resolver instance.
2020
### Changed
2121
- Extract model sorting from the Eloquent adapter into a `SortsModels` trait.
2222

23+
### Fixed
24+
- [#222](https://github.com/cloudcreativity/laravel-json-api/issues/222)
25+
Adding related resources to a has-many relationship now does not add duplicates. The JSON API
26+
spec states that duplicates must not be added, but the default Laravel behaviour does add duplicates.
27+
The `HasMany` relationship now takes care of this by filtering out duplicates before adding them.
28+
2329
### Deprecated
2430
- The following methods on the Eloquent adapter will be removed in `1.0.0` as they are no longer required:
2531
- `extractIncludePaths`

src/Eloquent/HasMany.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ public function replace($record, RelationshipInterface $relationship, EncodingPa
6767
}
6868

6969
/**
70+
* Add records to the relationship.
71+
*
72+
* Note that the spec says that duplicates MUST NOT be added. The default Laravel
73+
* behaviour is to add duplicates, therefore we need to do some work to ensure
74+
* that we only add the records that are not already in the relationship.
75+
*
7076
* @param Model $record
7177
* @param RelationshipInterface $relationship
7278
* @param EncodingParametersInterface $parameters
@@ -75,8 +81,14 @@ public function replace($record, RelationshipInterface $relationship, EncodingPa
7581
public function add($record, RelationshipInterface $relationship, EncodingParametersInterface $parameters)
7682
{
7783
$related = $this->findRelated($record, $relationship);
84+
$relation = $this->getRelation($record, $this->key);
85+
86+
$existing = $relation
87+
->getQuery()
88+
->whereKey($related->modelKeys())
89+
->get();
7890

79-
$this->getRelation($record, $this->key)->saveMany($related);
91+
$relation->saveMany($related->diff($existing));
8092
$record->refresh(); // in case the relationship has been cached.
8193

8294
return $record;

tests/lib/Integration/Eloquent/MorphToManyTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,31 @@ public function testAddToRelationship()
330330
$this->assertTagsAre($post, $existing->merge($add));
331331
}
332332

333+
/**
334+
* From the spec:
335+
*
336+
* > If a client makes a POST request to a URL from a relationship link,
337+
* > the server MUST add the specified members to the relationship unless
338+
* > they are already present. If a given type and id is already in the
339+
* > relationship, the server MUST NOT add it again.
340+
*/
341+
public function testAddToRelationshipDoesNotCreateDuplicates()
342+
{
343+
$post = factory(Post::class)->create();
344+
$existing = factory(Tag::class, 2)->create();
345+
$post->tags()->sync($existing);
346+
347+
$add = factory(Tag::class, 2)->create();
348+
$data = $add->merge($existing)->map(function (Tag $tag) {
349+
return ['type' => 'tags', 'id' => $tag->getRouteKey()];
350+
})->all();
351+
352+
$this->doAddToRelationship($post, 'tags', $data)
353+
->assertStatus(204);
354+
355+
$this->assertTagsAre($post, $existing->merge($add));
356+
}
357+
333358
public function testRemoveFromRelationship()
334359
{
335360
$post = factory(Post::class)->create();
@@ -346,6 +371,29 @@ public function testRemoveFromRelationship()
346371
$this->assertTagsAre($post, [$tags->get(2), $tags->get(3)]);
347372
}
348373

374+
/**
375+
* From the spec:
376+
*
377+
* > If all of the specified resources are able to be removed from,
378+
* > or are already missing from, the relationship then the server
379+
* > MUST return a successful response.
380+
*/
381+
public function testRemoveWithIdsThatAreNotRelated()
382+
{
383+
$post = factory(Post::class)->create();
384+
$tags = factory(Tag::class, 2)->create();
385+
$post->tags()->sync($tags);
386+
387+
$data = factory(Tag::class, 2)->create()->map(function (Tag $tag) {
388+
return ['type' => 'tags', 'id' => $tag->getRouteKey()];
389+
})->all();
390+
391+
$this->doRemoveFromRelationship($post, 'tags', $data)
392+
->assertStatus(204);
393+
394+
$this->assertTagsAre($post, $tags);
395+
}
396+
349397
/**
350398
* @param $post
351399
* @param $tag

0 commit comments

Comments
 (0)
0