Skip to content

Commit e7a808e

Browse files
committed
[Feature] Allow client-generated ids
1 parent 2a2e9f4 commit e7a808e

File tree

11 files changed

+440
-4
lines changed

11 files changed

+440
-4
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
/*
3+
* Copyright 2021 Cloud Creativity Limited
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace App\Http\Controllers\Api\V1;
21+
22+
use LaravelJsonApi\Laravel\Http\Controllers\JsonApiController;
23+
24+
class VideoController extends JsonApiController
25+
{
26+
}

tests/dummy/app/JsonApi/V1/Server.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
use App\JsonApi\V1\Posts\PostScope;
2323
use App\Models\Post;
24+
use App\Models\Video;
2425
use Illuminate\Support\Facades\Auth;
2526
use LaravelJsonApi\Core\Server\Server as BaseServer;
2627

@@ -45,6 +46,10 @@ public function serving(): void
4546
Post::creating(static function (Post $post) {
4647
$post->author()->associate(Auth::user());
4748
});
49+
50+
Video::creating(static function (Video $video) {
51+
$video->owner()->associate(Auth::user());
52+
});
4853
}
4954

5055
/**
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
/**
3+
* Copyright 2020 Cloud Creativity Limited
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace App\JsonApi\V1\Videos;
21+
22+
use LaravelJsonApi\Laravel\Http\Requests\ResourceRequest;
23+
use LaravelJsonApi\Validation\Rule as JsonApiRule;
24+
25+
class VideoRequest extends ResourceRequest
26+
{
27+
28+
/**
29+
* @return array
30+
*/
31+
public function rules(): array
32+
{
33+
return [
34+
'id' => ['nullable', JsonApiRule::clientId()],
35+
'tags' => JsonApiRule::toMany(),
36+
'title' => ['required', 'string'],
37+
'url' => ['required', 'string'],
38+
];
39+
}
40+
41+
}

tests/dummy/app/JsonApi/V1/Videos/VideoResource.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public function attributes(): iterable
4343
public function relationships(): iterable
4444
{
4545
return [
46-
$this->relation('tags'),
46+
$this->relation('tags')->showDataIfLoaded(),
4747
];
4848
}
4949

tests/dummy/app/JsonApi/V1/Videos/VideoSchema.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ class VideoSchema extends Schema
4545
public function fields(): array
4646
{
4747
return [
48-
ID::make(),
48+
ID::make()->uuid()->clientIds(),
4949
DateTime::make('createdAt')->sortable()->readOnly(),
50-
BelongsToMany::make('tags')->readOnly(),
50+
BelongsToMany::make('tags'),
5151
Str::make('title')->sortable(),
5252
DateTime::make('updatedAt')->sortable()->readOnly(),
5353
Str::make('url'),

tests/dummy/app/Models/Video.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,50 @@
2121

2222
use Illuminate\Database\Eloquent\Factories\HasFactory;
2323
use Illuminate\Database\Eloquent\Model;
24+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
2425
use Illuminate\Database\Eloquent\Relations\MorphToMany;
26+
use Illuminate\Support\Str;
2527

2628
class Video extends Model
2729
{
2830

2931
use HasFactory;
3032

33+
/**
34+
* @var bool
35+
*/
36+
public $incrementing = false;
37+
38+
/**
39+
* @var string
40+
*/
41+
protected $primaryKey = 'uuid';
42+
3143
/**
3244
* @var string[]
3345
*/
3446
protected $fillable = ['title', 'url'];
3547

48+
/**
49+
* @inheritDoc
50+
*/
51+
protected static function booting()
52+
{
53+
parent::booting();
54+
55+
self::creating(static function (self $model) {
56+
$model->{$model->getKeyName()} = $model->{$model->getKeyName()} ?? Str::uuid()->toString();
57+
});
58+
}
59+
60+
/**
61+
* @return BelongsTo
62+
*/
63+
public function owner(): BelongsTo
64+
{
65+
return $this->belongsTo(User::class);
66+
}
67+
3668
/**
3769
* @return MorphToMany
3870
*/
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
/*
3+
* Copyright 2020 Cloud Creativity Limited
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace App\Policies;
21+
22+
use App\Models\Post;
23+
use App\Models\Tag;
24+
use App\Models\User;
25+
use App\Models\Video;
26+
use LaravelJsonApi\Core\Store\LazyRelation;
27+
28+
class VideoPolicy
29+
{
30+
31+
/**
32+
* @param User|null $user
33+
* @return bool
34+
*/
35+
public function viewAny(?User $user): bool
36+
{
37+
return true;
38+
}
39+
40+
/**
41+
* @param User|null $user
42+
* @param Video $video
43+
* @return bool
44+
*/
45+
public function view(?User $user, Video $video): bool
46+
{
47+
return true;
48+
}
49+
50+
/**
51+
* @param User|null $user
52+
* @param Video $video
53+
* @return bool
54+
*/
55+
public function viewTags(?User $user, Video $video): bool
56+
{
57+
return $this->view($user, $video);
58+
}
59+
60+
/**
61+
* @param User|null $user
62+
* @return bool
63+
*/
64+
public function create(?User $user): bool
65+
{
66+
return !!$user;
67+
}
68+
69+
/**
70+
* @param User|null $user
71+
* @param Video $video
72+
* @return bool
73+
*/
74+
public function update(?User $user, Video $video): bool
75+
{
76+
return $this->owner($user, $video);
77+
}
78+
79+
/**
80+
* @param User|null $user
81+
* @param Video $video
82+
* @param LazyRelation $tags
83+
* @return bool
84+
*/
85+
public function updateTags(?User $user, Video $video, LazyRelation $tags): bool
86+
{
87+
$tags->collect()->each(fn(Tag $tag) => $tag);
88+
89+
return $this->owner($user, $video);
90+
}
91+
92+
/**
93+
* @param User|null $user
94+
* @param Video $video
95+
* @param LazyRelation $tags
96+
* @return bool
97+
*/
98+
public function attachTags(?User $user, Video $video, LazyRelation $tags): bool
99+
{
100+
return $this->updateTags($user, $video, $tags);
101+
}
102+
103+
/**
104+
* @param User|null $user
105+
* @param Video $video
106+
* @param LazyRelation $tags
107+
* @return bool
108+
*/
109+
public function detachTags(?User $user, Video $video, LazyRelation $tags): bool
110+
{
111+
return $this->updateTags($user, $video, $tags);
112+
}
113+
114+
/**
115+
* @param User|null $user
116+
* @param Video $video
117+
* @return bool
118+
*/
119+
public function delete(?User $user, Video $video): bool
120+
{
121+
return $this->owner($user, $video);
122+
}
123+
124+
/**
125+
* @param User|null $user
126+
* @param Video $video
127+
* @return bool
128+
*/
129+
public function owner(?User $user, Video $video): bool
130+
{
131+
return $user && $video->owner->is($user);
132+
}
133+
}

tests/dummy/database/migrations/2020_06_13_143800_create_post_and_video_tables.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,17 @@ public function up(): void
4747
});
4848

4949
Schema::create('videos', function (Blueprint $table) {
50-
$table->id();
50+
$table->uuid('uuid')->primary();
5151
$table->timestamps();
52+
$table->unsignedBigInteger('owner_id');
5253
$table->string('title');
5354
$table->string('url');
55+
56+
$table->foreign('owner_id')
57+
->references('id')
58+
->on('users')
59+
->onDelete('cascade')
60+
->onUpdate('cascade');
5461
});
5562

5663
Schema::create('comments', function (Blueprint $table) {

tests/dummy/routes/api.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@
88
$relationships->hasMany('comments')->readOnly();
99
$relationships->hasMany('tags');
1010
});
11+
12+
$server->resource('videos')->relationships(function ($relationships) {
13+
$relationships->hasMany('tags');
14+
});
1115
});

tests/dummy/tests/Api/V1/Posts/CreateTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,31 @@ public function testInvalid(): void
110110
$response->assertExactErrorStatus($expected);
111111
}
112112

113+
public function testClientId(): void
114+
{
115+
$post = Post::factory()->make();
116+
117+
$data = $this
118+
->serialize($post)
119+
->withId('81166677-f3c4-440c-9a4a-12b89802d731')
120+
->jsonSerialize();
121+
122+
$expected = [
123+
'detail' => "Resource type posts does not support client-generated IDs.",
124+
'source' => ['pointer' => '/data/id'],
125+
'status' => '403',
126+
'title' => 'Not Supported',
127+
];
128+
129+
$response = $this
130+
->actingAs($post->author)
131+
->jsonApi('posts')
132+
->withData($data)
133+
->post('/api/v1/posts');
134+
135+
$response->assertExactErrorStatus($expected);
136+
}
137+
113138
public function testUnauthorized(): void
114139
{
115140
$post = Post::factory()->make();

0 commit comments

Comments
 (0)