diff --git a/docs/docs/3.0/repository-pattern/repository-pattern.md b/docs/docs/3.0/repository-pattern/repository-pattern.md index 7b5cd7360..c53c127ad 100644 --- a/docs/docs/3.0/repository-pattern/repository-pattern.md +++ b/docs/docs/3.0/repository-pattern/repository-pattern.md @@ -43,6 +43,8 @@ You have available the follow endpoints: | GET | `/restify-api/posts` | index | | GET | `/restify-api/posts/{post}` | show | | POST | `/restify-api/posts` | store | +| POST | `/restify-api/posts/bulk` | store multiple | +| POST | `/restify-api/posts/bulk/update` | store multiple | | PATCH | `/restify-api/posts/{post}` | update | | PUT | `/restify-api/posts/{post}` | update | | POST | `/restify-api/posts/{post}` | update | @@ -133,7 +135,7 @@ public static function collectMiddlewares(RestifyRequest $request): ?Collection ## Dependency injection -The Laravel [service container](https://laravel.com/docs/6.x/container) is used to resolve all Laravel Restify repositories. +The Laravel [service container](https://laravel.com/docs/7.x/container) is used to resolve all Laravel Restify repositories. As a result, you are able to type-hint any dependencies your `Repository` may need in its constructor. The declared dependencies will automatically be resolved and injected into the repository instance: @@ -208,6 +210,15 @@ entire logic of a specific action. Let's say your `save` method has to do someth } ``` +### store bulk + +```php + public function storeBulk(Binaryk\LaravelRestify\Http\Requests\RepositoryStoreBulkRequest $request) + { + // Silence is golden + } +``` + ### update ```php @@ -217,6 +228,17 @@ entire logic of a specific action. Let's say your `save` method has to do someth } ``` +### update bulk + +// $row is the payload row to be updated + +```php + public function updateBulk(RestifyRequest $request, $repositoryId, int $row) + { + // Silence is golden + } +``` + ### destroy ```php @@ -592,4 +614,71 @@ you may want to force eager load a relationship in terms of using it in fields, public static $with = ['posts']; ``` +## Store bulk flow + +However, the `store` method is a common one, the `store bulk` requires a bit of attention. + +### Bulk field validations + +Similar with `store` and `update` methods, `bulk` rules has their own field rule definition: + +```php +->storeBulkRules('required', function () {}, Rule::in('posts:id')) +``` + +The validation rules will be merged with the rules provided into the `rules()` method. The validation will be performed +by using native Laravel validator, so you will have exactly the same experience. The validation `messages` could still be used as usual. + +### Bulk Payload + +The payload for a bulk store should contain an array of objects: + +```json +[ + { + "title": "First post" + }, + { + "title": "Second post" + } +] +``` + +### Bulk after store + +After storing an entity, the repository will call the static `bulkStored` method from the repository, so you can override: + +```php +public static function storedBulk(Collection $repositories, $request) +{ + // +} +``` + +## Update bulk flow + +As the store bulk, the update bulk uses DB transaction to perform the action. So you can make sure that even all entries, even no one where updated. + +### Bulk update field validations + +```php +->updateBulkRules('required', function () {}, Rule::in('posts:id')) +``` + +### Bulk Payload + +The payload for a bulk update should contain an array of objects. Each object SHOULD contain an `id` key, based on this, the Laravel Restify will find the entity: + +```json +[ + { + "id": 1, + "title": "First post" + }, + { + "id": 2, + "title": "Second post" + } +] +``` diff --git a/routes/api.php b/routes/api.php index dbfc496ce..e68b7979f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -10,7 +10,9 @@ use Binaryk\LaravelRestify\Http\Controllers\RepositoryFilterController; use Binaryk\LaravelRestify\Http\Controllers\RepositoryIndexController; use Binaryk\LaravelRestify\Http\Controllers\RepositoryShowController; +use Binaryk\LaravelRestify\Http\Controllers\RepositoryStoreBulkController; use Binaryk\LaravelRestify\Http\Controllers\RepositoryStoreController; +use Binaryk\LaravelRestify\Http\Controllers\RepositoryUpdateBulkController; use Binaryk\LaravelRestify\Http\Controllers\RepositoryUpdateController; use Illuminate\Support\Facades\Route; @@ -27,6 +29,8 @@ // API CRUD Route::get('/{repository}', '\\'.RepositoryIndexController::class); Route::post('/{repository}', '\\'.RepositoryStoreController::class); +Route::post('/{repository}/bulk', '\\'.RepositoryStoreBulkController::class); +Route::post('/{repository}/bulk/update', '\\'.RepositoryUpdateBulkController::class); Route::get('/{repository}/{repositoryId}', '\\'.RepositoryShowController::class); Route::patch('/{repository}/{repositoryId}', '\\'.RepositoryUpdateController::class); Route::put('/{repository}/{repositoryId}', '\\'.RepositoryUpdateController::class); diff --git a/src/Commands/stubs/policy.stub b/src/Commands/stubs/policy.stub index 43746bc56..7e26d179e 100644 --- a/src/Commands/stubs/policy.stub +++ b/src/Commands/stubs/policy.stub @@ -44,6 +44,17 @@ class {{ class }} // } + /** + * Determine whether the user can create multiple models at once. + * + * @param \App\User $user + * @return mixed + */ + public function storeBulk(User $user) + { + // + } + /** * Determine whether the user can update the model. * diff --git a/src/Fields/Field.php b/src/Fields/Field.php index dcc5ad0e9..fedfbc3a6 100644 --- a/src/Fields/Field.php +++ b/src/Fields/Field.php @@ -61,6 +61,12 @@ class Field extends OrganicField implements JsonSerializable */ public $storeCallback; + /** + * Callback called when the value is filled from a store bulk, this callback will do not override the fill action. + * @var Closure + */ + public $storeBulkCallback; + /** * Callback called when update. * @var Closure @@ -165,6 +171,13 @@ public function storeCallback(Closure $callback) return $this; } + public function storeCallbackCallback(Closure $callback) + { + $this->storeBulkCallback = $callback; + + return $this; + } + public function updateCallback(Closure $callback) { $this->updateCallback = $callback; @@ -191,9 +204,10 @@ public function fillCallback(Closure $callback) * * @param RestifyRequest $request * @param $model + * @param int|null $bulkRow * @return mixed|void */ - public function fillAttribute(RestifyRequest $request, $model) + public function fillAttribute(RestifyRequest $request, $model, int $bulkRow = null) { $this->resolveValueBeforeUpdate($request, $model); @@ -205,28 +219,34 @@ public function fillAttribute(RestifyRequest $request, $model) if (! $this->isHidden($request) && isset($this->fillCallback)) { return call_user_func( - $this->fillCallback, $request, $model, $this->attribute + $this->fillCallback, $request, $model, $this->attribute, $bulkRow ); } if (isset($this->appendCallback)) { - return $this->fillAttributeFromAppend($request, $model, $this->attribute); + return $this->fillAttributeFromAppend($request, $model, $this->attribute, $bulkRow); } if ($request->isStoreRequest() && is_callable($this->storeCallback)) { return call_user_func( - $this->storeCallback, $request, $model, $this->attribute + $this->storeCallback, $request, $model, $this->attribute, $bulkRow + ); + } + + if ($request->isStoreBulkRequest() && is_callable($this->storeBulkCallback)) { + return call_user_func( + $this->storeBulkCallback, $request, $model, $this->attribute, $bulkRow ); } if ($request->isUpdateRequest() && is_callable($this->updateCallback)) { return call_user_func( - $this->updateCallback, $request, $model, $this->attribute + $this->updateCallback, $request, $model, $this->attribute, $bulkRow ); } $this->fillAttributeFromRequest( - $request, $model, $this->attribute + $request, $model, $this->attribute, $bulkRow ); } @@ -236,11 +256,22 @@ public function fillAttribute(RestifyRequest $request, $model) * @param RestifyRequest $request * @param $model * @param $attribute + * @param int|null $bulkRow */ - protected function fillAttributeFromRequest(RestifyRequest $request, $model, $attribute) + protected function fillAttributeFromRequest(RestifyRequest $request, $model, $attribute, int $bulkRow = null) { - if ($request->exists($attribute) || $request->get($attribute)) { - $model->{$attribute} = $request[$attribute] ?? $request->get($attribute); + if (is_null($bulkRow)) { + if ($request->exists($attribute) || $request->input($attribute)) { + $model->{$attribute} = $request[$attribute] ?? $request->input($attribute); + } + + return; + } + + $bulkableAttribute = $bulkRow.'.'.$attribute; + + if ($request->exists($bulkableAttribute) || $request->get($bulkableAttribute)) { + $model->{$attribute} = $request[$bulkableAttribute] ?? $request->get($bulkableAttribute); } } @@ -286,6 +317,20 @@ public function storingRules($rules) return $this; } + public function storeBulkRules($rules) + { + $this->storingBulkRules = ($rules instanceof Rule || is_string($rules)) ? func_get_args() : $rules; + + return $this; + } + + public function updateBulkRules($rules) + { + $this->updateBulkRules = ($rules instanceof Rule || is_string($rules)) ? func_get_args() : $rules; + + return $this; + } + /** * Alias for storingRules - to maintain it consistent. * @@ -334,11 +379,21 @@ public function getStoringRules(): array return array_merge($this->rules, $this->storingRules); } + public function getStoringBulkRules(): array + { + return array_merge($this->rules, $this->storingBulkRules); + } + public function getUpdatingRules(): array { return array_merge($this->rules, $this->updatingRules); } + public function getUpdatingBulkRules(): array + { + return array_merge($this->rules, $this->updateBulkRules); + } + /** * Resolve the field's value for display. * diff --git a/src/Fields/FieldCollection.php b/src/Fields/FieldCollection.php index 152afe843..1a38d4590 100644 --- a/src/Fields/FieldCollection.php +++ b/src/Fields/FieldCollection.php @@ -22,6 +22,13 @@ public function authorizedUpdate(Request $request): self })->values(); } + public function authorizedUpdateBulk(Request $request): self + { + return $this->filter(function (OrganicField $field) use ($request) { + return $field->authorizedToUpdateBulk($request); + })->values(); + } + public function authorizedStore(Request $request): self { return $this->filter(function (OrganicField $field) use ($request) { @@ -57,10 +64,24 @@ public function forStore(RestifyRequest $request, $repository): self })->values(); } + public function forStoreBulk(RestifyRequest $request, $repository): self + { + return $this->filter(function (Field $field) use ($repository, $request) { + return $field->isShownOnStoreBulk($request, $repository); + })->values(); + } + public function forUpdate(RestifyRequest $request, $repository): self { return $this->filter(function (Field $field) use ($repository, $request) { return $field->isShownOnUpdate($request, $repository); })->values(); } + + public function forUpdateBulk(RestifyRequest $request, $repository): self + { + return $this->filter(function (Field $field) use ($repository, $request) { + return $field->isShownOnUpdateBulk($request, $repository); + })->values(); + } } diff --git a/src/Fields/OrganicField.php b/src/Fields/OrganicField.php index e41cfedfe..3ad937502 100644 --- a/src/Fields/OrganicField.php +++ b/src/Fields/OrganicField.php @@ -12,6 +12,8 @@ abstract class OrganicField extends BaseField public $canUpdateCallback; + public $canUpdateBulkCallback; + public $canStoreCallback; public $readonlyCallback; @@ -22,6 +24,10 @@ abstract class OrganicField extends BaseField public array $storingRules = []; + public array $storingBulkRules = []; + + public array $updateBulkRules = []; + public array $updatingRules = []; public array $messages = []; @@ -115,6 +121,11 @@ public function authorizedToUpdate(Request $request) return $this->canUpdateCallback ? call_user_func($this->canUpdateCallback, $request) : true; } + public function authorizedToUpdateBulk(Request $request) + { + return $this->canUpdateBulkCallback ? call_user_func($this->canUpdateBulkCallback, $request) : true; + } + public function authorizedToStore(Request $request) { return $this->canStoreCallback ? call_user_func($this->canStoreCallback, $request) : true; @@ -134,6 +145,13 @@ public function canUpdate(Closure $callback) return $this; } + public function canUpdateBulk(Closure $callback) + { + $this->canUpdateBulkCallback = $callback; + + return $this; + } + public function canStore(Closure $callback) { $this->canStoreCallback = $callback; @@ -164,11 +182,21 @@ public function isShownOnUpdate(RestifyRequest $request, $repository): bool return ! $this->isReadonly($request); } + public function isShownOnUpdateBulk(RestifyRequest $request, $repository): bool + { + return ! $this->isReadonly($request); + } + public function isShownOnStore(RestifyRequest $request, $repository): bool { return ! $this->isReadonly($request); } + public function isShownOnStoreBulk(RestifyRequest $request, $repository): bool + { + return ! $this->isReadonly($request); + } + public function isHidden(RestifyRequest $request) { return with($this->hiddenCallback, function ($callback) use ($request) { diff --git a/src/Http/Controllers/RepositoryStoreBulkController.php b/src/Http/Controllers/RepositoryStoreBulkController.php new file mode 100644 index 000000000..4752cbb82 --- /dev/null +++ b/src/Http/Controllers/RepositoryStoreBulkController.php @@ -0,0 +1,15 @@ +repository() + ->allowToBulkStore($request) + ->storeBulk($request); + } +} diff --git a/src/Http/Controllers/RepositoryUpdateBulkController.php b/src/Http/Controllers/RepositoryUpdateBulkController.php new file mode 100644 index 000000000..82fdcdcc5 --- /dev/null +++ b/src/Http/Controllers/RepositoryUpdateBulkController.php @@ -0,0 +1,34 @@ +collectInput() + ->each(function (array $item, int $row) use ($request) { + $model = $request->findModelQuery( + $id = $item['id'] + )->lockForUpdate()->firstOrFail(); + + /** * @var Repository $repository */ + $repository = $request->newRepositoryWith($model); + + return $repository + ->allowToUpdateBulk($request) + ->updateBulk( + $request, $id, $row + ); + }); + }); + + return $this->response() + ->success(); + } +} diff --git a/src/Http/Requests/RepositoryStoreBulkRequest.php b/src/Http/Requests/RepositoryStoreBulkRequest.php new file mode 100644 index 000000000..87a23ea70 --- /dev/null +++ b/src/Http/Requests/RepositoryStoreBulkRequest.php @@ -0,0 +1,15 @@ +all(), + ); + } +} diff --git a/src/Http/Requests/RepositoryUpdateBulkRequest.php b/src/Http/Requests/RepositoryUpdateBulkRequest.php new file mode 100644 index 000000000..d9d6b06cb --- /dev/null +++ b/src/Http/Requests/RepositoryUpdateBulkRequest.php @@ -0,0 +1,15 @@ +all(), + ); + } +} diff --git a/src/Http/Requests/RestifyRequest.php b/src/Http/Requests/RestifyRequest.php index 3b6c8c531..f56a08c4c 100644 --- a/src/Http/Requests/RestifyRequest.php +++ b/src/Http/Requests/RestifyRequest.php @@ -58,6 +58,16 @@ public function isStoreRequest() return $this instanceof RepositoryStoreRequest; } + public function isStoreBulkRequest() + { + return $this instanceof RepositoryStoreBulkRequest; + } + + public function isUpdateBulkRequest() + { + return $this instanceof RepositoryUpdateBulkRequest; + } + public function isViaRepository() { return $this->viaRepository && $this->viaRepositoryId; diff --git a/src/Repositories/Repository.php b/src/Repositories/Repository.php index 0c7bcd09a..3704596fb 100644 --- a/src/Repositories/Repository.php +++ b/src/Repositories/Repository.php @@ -8,6 +8,7 @@ use Binaryk\LaravelRestify\Fields\Field; use Binaryk\LaravelRestify\Fields\FieldCollection; use Binaryk\LaravelRestify\Filter; +use Binaryk\LaravelRestify\Http\Requests\RepositoryStoreBulkRequest; use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; use Binaryk\LaravelRestify\Restify; use Binaryk\LaravelRestify\Services\Search\RepositorySearchService; @@ -255,6 +256,14 @@ public function collectFields(RestifyRequest $request) $method = 'fieldsForStore'; } + if ($request->isStoreBulkRequest() && method_exists($this, 'fieldsForStoreBulk')) { + $method = 'fieldsForStoreBulk'; + } + + if ($request->isUpdateBulkRequest() && method_exists($this, 'fieldsForUpdateBulk')) { + $method = 'fieldsForUpdateBulk'; + } + $fields = FieldCollection::make(array_values($this->filter($this->{$method}($request)))); if ($this instanceof Mergeable) { @@ -289,6 +298,13 @@ private function updateFields(RestifyRequest $request) ->authorizedUpdate($request); } + private function updateBulkFields(RestifyRequest $request) + { + return $this->collectFields($request) + ->forUpdateBulk($request, $this) + ->authorizedUpdateBulk($request); + } + private function storeFields(RestifyRequest $request) { return $this->collectFields($request) @@ -296,6 +312,13 @@ private function storeFields(RestifyRequest $request) ->authorizedStore($request); } + private function storeBulkFields(RestifyRequest $request) + { + return $this->collectFields($request) + ->forStoreBulk($request, $this) + ->authorizedStore($request); + } + /** * @param $resource * @return Repository @@ -589,6 +612,31 @@ public function store(RestifyRequest $request) ->header('Location', static::uriTo($this->resource)); } + public function storeBulk(RepositoryStoreBulkRequest $request) + { + $entities = DB::transaction(function () use ($request) { + return $request->collectInput() + ->map(function (array $input, $row) use ($request) { + $this->resource = static::newModel(); + + static::fillBulkFields( + $request, $this->resource, $this->storeBulkFields($request), $row + ); + + $this->resource->save(); + + $this->storeBulkFields($request)->each(fn (Field $field) => $field->invokeAfter($request, $this->resource)); + + return $this->resource; + }); + }); + + static::storedBulk($entities, $request); + + return $this->response() + ->created(); + } + public function update(RestifyRequest $request, $repositoryId) { $this->resource = DB::transaction(function () use ($request) { @@ -606,6 +654,18 @@ public function update(RestifyRequest $request, $repositoryId) ->success(); } + public function updateBulk(RestifyRequest $request, $repositoryId, int $row) + { + $fields = $this->updateBulkFields($request); + + static::fillBulkFields($request, $this->resource, $fields, $row); + + $this->resource->save(); + + return $this->response() + ->success(); + } + public function attach(RestifyRequest $request, $repositoryId, Collection $pivots) { DB::transaction(function () use ($request, $pivots) { @@ -651,6 +711,17 @@ public function allowToUpdate(RestifyRequest $request, $payload = null): self return $this; } + public function allowToUpdateBulk(RestifyRequest $request, $payload = null): self + { + $this->authorizeToUpdateBulk($request); + + $validator = static::validatorForUpdateBulk($request, $this, $payload); + + $validator->validate(); + + return $this; + } + public function allowToStore(RestifyRequest $request, $payload = null): self { static::authorizeToStore($request); @@ -662,6 +733,17 @@ public function allowToStore(RestifyRequest $request, $payload = null): self return $this; } + public function allowToBulkStore(RestifyRequest $request, $payload = null): self + { + static::authorizeToStoreBulk($request); + + $validator = static::validatorForStoringBulk($request, $payload); + + $validator->validate(); + + return $this; + } + public function allowToDestroy(RestifyRequest $request) { $this->authorizeToDelete($request); @@ -681,6 +763,11 @@ public static function stored($repository, $request) // } + public static function storedBulk(Collection $repositories, $request) + { + // + } + public static function updated($model, $request) { // @@ -753,6 +840,13 @@ protected static function fillFields(RestifyRequest $request, Model $model, Coll }); } + protected static function fillBulkFields(RestifyRequest $request, Model $model, Collection $fields, int $bulkRow = null) + { + return $fields->map(function (Field $field) use ($request, $model, $bulkRow) { + return $field->fillAttribute($request, $model, $bulkRow); + }); + } + public static function uriTo(Model $model) { return Restify::path().'/'.static::uriKey().'/'.$model->getKey(); diff --git a/src/Repositories/ValidatingTrait.php b/src/Repositories/ValidatingTrait.php index 558507992..9dfb18eeb 100644 --- a/src/Repositories/ValidatingTrait.php +++ b/src/Repositories/ValidatingTrait.php @@ -13,7 +13,7 @@ trait ValidatingTrait { /** - * @param RestifyRequest $request + * @param RestifyRequest $request * @return Collection */ abstract public function collectFields(RestifyRequest $request); @@ -30,6 +30,7 @@ abstract public static function newModel(); */ public static function validatorForStoring(RestifyRequest $request, array $plainPayload = null) { + /** * @var Repository $on */ $on = static::resolveWith(static::newModel()); $messages = $on->collectFields($request)->flatMap(function ($k) { @@ -47,24 +48,39 @@ public static function validatorForStoring(RestifyRequest $request, array $plain }); } + public static function validatorForStoringBulk(RestifyRequest $request, array $plainPayload = null) + { + /** * @var Repository $on */ + $on = static::resolveWith(static::newModel()); + + $messages = $on->collectFields($request)->flatMap(function ($k) { + $messages = []; + foreach ($k->messages as $ruleFor => $message) { + $messages['*'.$k->attribute.'.'.$ruleFor] = $message; + } + + return $messages; + })->toArray(); + + return Validator::make($plainPayload ?? $request->all(), $on->getStoringBulkRules($request), $messages)->after(function ($validator) use ($request) { + static::afterValidation($request, $validator); + static::afterStoringBulkValidation($request, $validator); + }); + } + /** * Validate a resource update request. - * @param RestifyRequest $request - * @param null $resource + * @param RestifyRequest $request + * @param null $resource */ public static function validateForUpdate(RestifyRequest $request, $resource = null) { static::validatorForUpdate($request, $resource)->validate(); } - /** - * @param RestifyRequest $request - * @param null $resource - * @param array $plainPayload - * @return \Illuminate\Contracts\Validation\Validator - */ public static function validatorForUpdate(RestifyRequest $request, $resource = null, array $plainPayload = null) { + /** * @var Repository $on */ $on = $resource ?? static::resolveWith(static::newModel()); $messages = $on->collectFields($request)->flatMap(function ($k) { @@ -82,11 +98,31 @@ public static function validatorForUpdate(RestifyRequest $request, $resource = n }); } + public static function validatorForUpdateBulk(RestifyRequest $request, $resource = null, array $plainPayload = null) + { + /** * @var Repository $on */ + $on = $resource ?? static::resolveWith(static::newModel()); + + $messages = $on->collectFields($request)->flatMap(function ($k) { + $messages = []; + foreach ($k->messages as $ruleFor => $message) { + $messages['*'.$k->attribute.'.'.$ruleFor] = $message; + } + + return $messages; + })->toArray(); + + return Validator::make($plainPayload ?? $request->all(), $on->getUpdatingBulkRules($request), $messages)->after(function ($validator) use ($request) { + static::afterValidation($request, $validator); + static::afterUpdatingBulkValidation($request, $validator); + }); + } + /** * Handle any post-validation processing. * - * @param RestifyRequest $request - * @param \Illuminate\Validation\Validator $validator + * @param RestifyRequest $request + * @param \Illuminate\Validation\Validator $validator * @return void */ protected static function afterValidation(RestifyRequest $request, $validator) @@ -94,30 +130,24 @@ protected static function afterValidation(RestifyRequest $request, $validator) // } - /** - * Handle any post-storing validation processing. - * - * @param RestifyRequest $request - * @param \Illuminate\Validation\Validator $validator - * @return void - */ protected static function afterStoringValidation(RestifyRequest $request, $validator) { } - /** - * Handle any post-storing validation processing. - * - * @param RestifyRequest $request - * @param \Illuminate\Validation\Validator $validator - * @return void - */ + protected static function afterStoringBulkValidation(RestifyRequest $request, $validator) + { + } + protected static function afterUpdatingValidation(RestifyRequest $request, $validator) { } + protected static function afterUpdatingBulkValidation(RestifyRequest $request, $validator) + { + } + /** - * @param RestifyRequest $request + * @param RestifyRequest $request * @return array */ public function getStoringRules(RestifyRequest $request) @@ -129,10 +159,15 @@ public function getStoringRules(RestifyRequest $request) })->toArray(); } - /** - * @param RestifyRequest $request - * @return array - */ + public function getStoringBulkRules(RestifyRequest $request) + { + return $this->collectFields($request)->mapWithKeys(function (Field $k) { + return [ + "*.{$k->attribute}" => $k->getStoringBulkRules(), + ]; + })->toArray(); + } + public function getUpdatingRules(RestifyRequest $request) { return $this->collectFields($request)->mapWithKeys(function (Field $k) { @@ -141,4 +176,13 @@ public function getUpdatingRules(RestifyRequest $request) ]; })->toArray(); } + + public function getUpdatingBulkRules(RestifyRequest $request) + { + return $this->collectFields($request)->mapWithKeys(function (Field $k) { + return [ + "*.{$k->attribute}" => $k->getUpdatingBulkRules(), + ]; + })->toArray(); + } } diff --git a/src/Traits/AuthorizableModels.php b/src/Traits/AuthorizableModels.php index 510fb5783..bbd9acf2b 100644 --- a/src/Traits/AuthorizableModels.php +++ b/src/Traits/AuthorizableModels.php @@ -98,6 +98,13 @@ public static function authorizeToStore(Request $request) } } + public static function authorizeToStoreBulk(Request $request) + { + if (! static::authorizedToStoreBulk($request)) { + throw new AuthorizationException('Unauthorized to store bulk.'); + } + } + /** * Determine if the current user can store new repositories. * @@ -113,6 +120,15 @@ public static function authorizedToStore(Request $request) return true; } + public static function authorizedToStoreBulk(Request $request) + { + if (static::authorizable()) { + return Gate::check('storeBulk', static::$model); + } + + return true; + } + /** * Determine if the current user can update the given resource or throw an exception. * @@ -126,6 +142,11 @@ public function authorizeToUpdate(Request $request) $this->authorizeTo($request, 'update'); } + public function authorizeToUpdateBulk(Request $request) + { + $this->authorizeTo($request, 'updateBulk'); + } + /** * Determine if the current user can update the given resource. * diff --git a/tests/Controllers/RepositoryStoreBulkControllerTest.php b/tests/Controllers/RepositoryStoreBulkControllerTest.php new file mode 100644 index 000000000..1290b30b1 --- /dev/null +++ b/tests/Controllers/RepositoryStoreBulkControllerTest.php @@ -0,0 +1,70 @@ +authenticate(); + } + + public function test_basic_validation_works() + { + $this->postJson('/restify-api/posts/bulk', [ + [ + 'title' => null, + ], + ]) + ->assertStatus(400) + ->assertJson([ + 'errors' => [ + [ + '0.title' => [ + 'This field is required', + ], + ], + ], + ]); + } + + public function test_unauthorized_store_bulk() + { + $_SERVER['restify.post.storeBulk'] = false; + + Gate::policy(Post::class, PostPolicy::class); + + $this->postJson('/restify-api/posts/bulk', [ + [ + 'title' => 'Title', + 'description' => 'Title', + ], + ])->assertStatus(403) + ->assertJson(['errors' => ['Unauthorized to store bulk.']]); + } + + public function test_user_can_bulk_create_posts() + { + $user = $this->mockUsers()->first(); + + $this->postJson('/restify-api/posts/bulk', [ + [ + 'user_id' => $user->id, + 'title' => 'First post.', + ], + [ + 'user_id' => $user->id, + 'title' => 'Second post.', + ], + ]) + ->assertStatus(201); + + $this->assertDatabaseCount('posts', 2); + } +} diff --git a/tests/Controllers/RepositoryUpdateBulkControllerTest.php b/tests/Controllers/RepositoryUpdateBulkControllerTest.php new file mode 100644 index 000000000..a4582d0c1 --- /dev/null +++ b/tests/Controllers/RepositoryUpdateBulkControllerTest.php @@ -0,0 +1,71 @@ +authenticate(); + } + + public function test_basic_update_validation_works() + { + $post1 = factory(Post::class)->create([ + 'user_id' => 1, + 'title' => 'First title', + ]); + + $this->post('/restify-api/posts/bulk/update', [ + [ + 'id' => $post1->id, + 'title' => null, + ], + ]) + ->assertStatus(400) + ->assertJson([ + 'errors' => [ + [ + '0.title' => [ + 'This field is required', + ], + ], + ], + ]); + } + + public function test_basic_update_works() + { + $post1 = factory(Post::class)->create([ + 'user_id' => 1, + 'title' => 'First title', + ]); + $post2 = factory(Post::class)->create([ + 'user_id' => 1, + 'title' => 'Second title', + ]); + + $this->post('/restify-api/posts/bulk/update', [ + [ + 'id' => $post1->id, + 'title' => 'Updated first title', + ], + [ + 'id' => $post2->id, + 'title' => 'Updated second title', + ], + ]) + ->assertStatus(200); + + $updatedPost = Post::find($post1->id); + $updatedPost2 = Post::find($post2->id); + + $this->assertEquals($updatedPost->title, 'Updated first title'); + $this->assertEquals($updatedPost2->title, 'Updated second title'); + } +} diff --git a/tests/Fixtures/Post/PostPolicy.php b/tests/Fixtures/Post/PostPolicy.php index ed4fda98d..8e95bf325 100644 --- a/tests/Fixtures/Post/PostPolicy.php +++ b/tests/Fixtures/Post/PostPolicy.php @@ -25,6 +25,14 @@ public function store($user) return $_SERVER['restify.post.creatable'] ?? true; } + /** + * Determine if posts can be stored bulk. + */ + public function storeBulk($user) + { + return $_SERVER['restify.post.storeBulk'] ?? true; + } + public function update($user, $post) { return $_SERVER['restify.post.updateable'] ?? true; diff --git a/tests/Fixtures/Post/PostRepository.php b/tests/Fixtures/Post/PostRepository.php index 729c52d54..c2b856314 100644 --- a/tests/Fixtures/Post/PostRepository.php +++ b/tests/Fixtures/Post/PostRepository.php @@ -51,6 +51,28 @@ public function fieldsForStore(RestifyRequest $request) ]; } + public function fieldsForStoreBulk(RestifyRequest $request) + { + return [ + Field::new('title')->storeBulkRules('required')->messages([ + 'required' => 'This field is required', + ]), + + Field::new('user_id'), + ]; + } + + public function fieldsForUpdateBulk(RestifyRequest $request) + { + return [ + Field::new('title')->updateBulkRules('required')->messages([ + 'required' => 'This field is required', + ]), + + Field::new('user_id'), + ]; + } + public function filters(RestifyRequest $request) { return [