diff --git a/docs-v2/content/en/api/actions.md b/docs-v2/content/en/api/actions.md index 1bad1f733..caf560899 100644 --- a/docs-v2/content/en/api/actions.md +++ b/docs-v2/content/en/api/actions.md @@ -5,38 +5,38 @@ category: API position: 9 --- -Restify allow you to define extra actions for your repositories. Let's say you have a list of posts, and you have to -publish them. Usually for this kind of operations, you have to define a custom route like: +## Motivation + +Besides, built in CRUD operations and filtering, Restify allows you to define extra actions for your repositories. + +Let's say you have a list of posts, and you have to publish them. Usually for this kind of operations, you have to define a custom route like: ```php -// PostRepository +$router->post('posts/publish', PublishPostsController::class); -public static function routes(Router $router, $attributes, $wrap = true) -{ - $router->post('/publish', [static::class, 'publishMultiple']); -} +// PublishPostsController.php -public function publishMultiple(RestifyRequest $request) +public function __invoke(RestifyRequest $request) { ... } ``` -There is nothing bad with this approach, but when project growth, you will notice that the routes / repository can -easily become a mess. +The `classic` approach is good, however, it has a few limitations. Firstly, you have to manually take care of the route `middleware`, the testability for these endpoints should be done separately which is hard to maintain. And finally, the endpoint is disconnected from the repository, which makes it feel out of context so has a bad readability. -More than that, you have to guess a route name or handler, should it be a controller, a callback or a method right in -the repository class? Well, for this kind of operations, actions is what we need. +So, code readability, testability and maintainability become hard. -# Defining Actions +## Action definition -The action could be generated by using the command: +The action is nothing more than a class, that extends the `Binaryk\LaravelRestify\Actions\Action` abstract class. + +It could be generated by using the command: ```bash -artisan restify:action PublishPostsAction +php artisan restify:action PublishPostsAction ``` -This will generate for the handler class: +This will generate the action class: ```php namespace App\Restify\Actions; @@ -50,265 +50,376 @@ class PublishPostAction extends Action { public function handle(ActionRequest $request, Collection $models): JsonResponse { - return $this->response()->respond(); + return response()->json(); } } ``` -The `$models` represents a collection with all of the models for this query. +The `$models` argument represents a collection of all the models for this query. -## Available actions +### Register action -The frontend which consume your API could check available actions by using exposed endpoint: +Then add the action instance to the repository `actions` method: + +```php +// PostRepository.php + +public function actions(RestifyRequest $request): array +{ + return [ + PublishPostAction::new() + ]; +} +``` + +### Authorize action + +You can authorize certain actions to be active for specific users: + +```php +public function actions(RestifyRequest $request): array +{ + return [ + PublishPostAction::new()->canSee(function (Request $request) { + return $request->user()->can('publishAnyPost', Post::class), + }), + ]; +} +``` + +### Call actions + +To call an action, you simply access: ```http request -GET: api/api/restify/posts/actions +POST: api/restify/posts/actions?action=publish-posts-action ``` -This will answer with a json like: +The `action` query param value is the `ke-bab` form of the filter class name by default, or a custom `$uriKey` [defined in the action](#custom-uri-key) + + +The payload could be any type of json data, however, if you're using an [index-action](#index-actions), you are required to pass the `repositories` key, which represents the list of model keys we apply the action: ```json { - "data": { - "name": "Publish Posts Action", - "destructive": false, - "uriKey": "publish-posts-action", - "payload": [] - } + "repositories": [1, 2] } ``` -`name` - humanized name of the action +### Handle action -`destructive` - you may extend the `Binaryk\LaravelRestify\Actions\DestructiveAction` to indicate to the frontend than -this action is destructive (could be used for deletions) +As soon the action is called, the handled method will be invoked with the `$request` and list of models matching the keys passed via `repositories`: -`uriKey` - is the key of the action, will be used to perform the action +```php +public function handle(ActionRequest $request, Collection $models) +{ + $models->each->publish(); -`payload` - a key / value object indicating required payload + return ok(); +} +``` -# Registering Actions +## Action customizations -Once you have defined the action, you can register it for many resources. +Actions could be easily customized. + +### Action index query + +Similarly to repository [index query](/repositories-advanced#index-query), we can do the same by adding the `indexQuery` method on the action: ```php -public function actions(RestifyRequest $request) +class PublishPostAction extends Action { - return [ - PublishPostAction::new(), - ]; + public static function indexQuery(RestifyRequest $request, $query) + { + $query->whereNotNull('published_at'); + } + + ... } ``` -You can pass anything to the action constructor: +This method will be called right before items are retrieved from the database, so you can filter out or eager load using your custom statements. + +### Custom uri key + +Since your class names could change along the way, you can define a `$uriKey` property to your actions, so the frontend will use always the same `action` query when applying an action: ```php -public function actions(RestifyRequest $request) +class PublishPostAction extends Action +{ + public static $uriKey = 'publish-posts'; + + //... + +}; +``` + +### Rules + +Similarly to [advanced filters rules](/search/advanced-filters#advanced-filter-rules), you could define rules for the action so the payload will get validated before the handle method is fired. + +```php +public function rules(): array { return [ - PublishPostAction::new("Publish articles.", app(Validator::class)), + 'active' => ['required', 'bool'], ]; } ``` - - -Repository model You may consider that you have access to the `$this->resource` in the `actions` method ( -since it's not static). However, in this method the `resource` is not available, since the request is for a new model. - + +Restify doesn't validate the payload automatically as it does for filters, you're free to validate the payload in the handle method. -### Unauthorized +Always validate the payload as early as possible in the `handle` method: -Actions could be authorized: ```php -public function actions(RestifyRequest $request) +public function handle(ActionRequest $request, Collection $models) { - return [ - PublishPostAction::new()->canSee(function (Request $request) { - return $request->user()->can('pubishAnyPost', Post::class), - }), - ]; + $request->validate($this->rules()); + + ... } ``` -### Authorizing Actions Per-Model +## Actions scope -As you saw, we don't have access to the repository model in the `actions` method. However, we do have access to models -in the handle method. You're free to use Laravel Policies there. +By default, any action could be used on [index](#index-actions) as well as on [show](#show-actions). However, you can choose to instruct your action to be displayed to a specific scope. -# Use actions +## Show actions -The usage of an action, means the `handle` method implementation. The first argument is the `RestifyRequest`, and the -second one is a Collection of models, matching the `repositories` payload. +Show actions are used when you have to apply it for a single item. -```http request -POST: api/api/restify/posts/actions?action=publish-posts-action -``` +### Show action definition -Payload: +The show action definition is different in the way it receives arguments for the `handle` method. -```json +Restify automatically resolves Eloquent models defined in the route id and passes it to the action's handle method: + +```php +// PublishPostAction.php + +public function handle(ActionRequest $request, Post $post): JsonResponse { - "repositories": [ - 1, - 2 - ] + } + ``` +### Show action registration + +To register a show action, we have to use the `->onlyOnShow()` accessor: + ```php -public function handle(ActionRequest $request, Collection $models): JsonResponse +public function actions(RestifyRequest $request) { - // $models contains 2 posts (under ids 1 and 2) - $models->each->publish(); - - return $this->response()->respond(); + return [ + PublishPostAction::new()->onlyOnShow(), + ]; } ``` -## Filters +### Show action call -You can apply any filter or eager loadings as for an usual request: +The post URL should include the key of the model we want Restify to resolve: ```http request -POST: api/api/restify/posts/actions?action=publish-posts-action&id=1&filters= +POST: api/restfiy/posts/1/actions?action=publish-post-action ``` -This will apply the match for the `id = 1` and `filter` along with the match for the `repositories` payload you're -sending. - -### Modify query - -Similar with the way we can modify the query applied to the repository, we can do the same by adding the `indexQuery` -method on the action: +The payload could be empty: -```php -class PublishPostAction extends Action -{ - public static function indexQuery(RestifyRequest $request, $query) - { - $query->whereNotNull('published_at'); - } - - //... -} +```json +{} ``` -## All +### List show actions -Sometimes you may need to apply an action for all models. For this you can send: +To get the list of available actions only for a specific model key: ```http request -{ repositories: "all" } +GET: api/api/restify/posts/1/actions ``` -Under the hood Restify will take by 200 chunks entries from the database and the handle method for these in a DB -transaction. You are free to modify this default number of chunks: +See [get available actions](#get-available-actions) for more details. + +## Index actions + +Index actions are used when you have to apply it for a many items. + +### Index action definition + +The index action definition is different in the way it receives arguments for the `handle` method. + +Restify automatically resolves Eloquent models sent via the `repositories` key sent into the call payload and passes it to the action's handle method as a collection of items: ```php -public static int $chunkCount = 150; +// PublishPostAction.php +use Illuminate\Support\Collection; + +public function handle(ActionRequest $request, Collection $posts): JsonResponse +{ + // +} + ``` -## Action visibility +### Index action registration -Usually you need an action only for a single model. Then you can use: +To register an index action, we have to use the `->onlyOnIndex()` accessor: ```php +// PostRepository.php + public function actions(RestifyRequest $request) { return [ - PublishPostAction::new()->onlyOnShow(), + PublishPostsAction::new()->onlyOnIndex(), ]; } ``` -And available actions only for a specific repository id could be listed like: +### Index action call + +The post URL: ```http request -GET: api/api/restify/posts/1/actions +POST: api/restfiy/posts/actions?action=publish-posts-action ``` -Having this in place, you now have access to the current repository in the `actions` method: +The payload should always include a key called `repositories`, which is an array of model keys or the `all` keyword if you want to get all: -```php -public function actions(RestifyRequest $request) +```json { - return [ - PublishPostAction::new()->onlyOnShow()->canSee(function(ActionRequest $request) { - return $request->user()->ownsPost($request->findModelOrFail()); - }) - ]; + "repositories": [1, 2, 3] +} +``` + +So Restify will resolve posts with ids in the list of `[1, 2, 3]`. + +### Apply index action for all + +You can apply the index action for all models from the database if you send the payload: + +```json +{ + "repositories": "all" } ``` -Performing this action, you can only for a single repository: +Restify will get chunks of 200 and send them into the `Collection` argument for the `handle` method. + +You can customize the chunk number by customizing the `chunkCount` action property: + +```php +// PublishPostAction.php + +public static int $chunkCount = 500; +``` + +### List index actions + +To get the list of available actions: ```http request -POST: api/api/restify/posts/1/actions?action=publish-posts-action +GET: api/api/restify/posts/actions ``` -And you don't have to pass the `repositories` array in that case, since it's present in the query. +See [get available actions](#get-available-actions) for more details. + +## Standalone actions -Because it will be right in your handle method: +Sometimes you don't need to have an action with models. Let's say for example the authenticated user wants to disable +his account. + +### Standalone action definition: + +The index action definition is different in the way it doesn't require the second argument for the `handle`. ```php -public function handle(ActionRequest $request, Post $post): JsonResponse +// DisableProfileAction.php + +public function handle(ActionRequest $request): JsonResponse { // } + ``` -## Standalone actions +### Standalone action registration -Sometimes you don't need to have an action with models. Let's say for example the authenticated user wants to disable -his account. For this we have `standalone` actions: +There are two ways to register the standalone action: ```php // UserRepository - public function actions(RestifyRequest $request) - { - return [ - DisableProfileAction::new()->standalone(), - ]; - } +public function actions(RestifyRequest $request) +{ + return [ + DisableProfileAction::new()->standalone(), + ]; +} ``` -Just mark it as standalone with `->standalone` or override the property directly into the action: +Using the `->standalone()` mutator or by overriding the `$standalone` action property directly into the action: ```php class DisableProfileAction extends Action { - public $standalone = true; + public bool $standalone = true; //... } ``` -## URI Key +### Standalone action call -Usually the URL for the action is make based on the action name. You can use your own URI key if you want: +To call a standalone action you're using a similar URL as for the [index action](#index-action-call) -```php -class DisableProfileAction extends Action -{ - public static $uriKey = 'disable_profile'; +```http request +POST: api/restfiy/users/actions?action=disable-profile-action +``` - //... -} +However, you are not required to pass the `repositories` payload key. + +### List standalone actions + +Standalone actions will be displayed on both [listing show actions](#list-show-actions) or [listing index actions](#list-index-actions). + +## Filters + +You can apply any search, match, filter or eager loadings as for a usual request: + +```http request +POST: api/api/restify/posts/actions?action=publish-posts-action&id=1&filters= ``` +This will apply the match for the `id = 1` and `filter` along with the match for the `repositories` payload you're +sending. + ## Action Log It is often useful to view a log of the actions that have been run against a model, or seeing when the model was -updated, deleted or created (and by whom). Thankfully, Restify makes it a breeze to add an action log to a model by -attaching the `Binaryk\LaravelRestify\Models\Concerns\HasActionLogs` trait to the repository's corresponding Eloquent -model. +updated, deleted or created (and by whom). + +Thankfully, Restify makes it a breeze to add an action log to a model by attaching the `Binaryk\LaravelRestify\Models\Concerns\HasActionLogs` trait to the repository's corresponding Eloquent model. + +### Activate logs + +Simply adding the `HasActionLogs` trait to your model, it will log all actions and CRUD operations into the database into the `action_logs` table: + +```php +// Post.php + +class Post extends Model +{ + use \Binaryk\LaravelRestify\Models\Concerns\HasActionLogs; +} +``` -Having `HasActionLogs` trait attached to your model, all of the actions and CRUD operations will be logged into the -database into the `action_logs` table. +### Display logs You can display them by attaching to the repository related for example: @@ -320,7 +431,7 @@ use Binaryk\LaravelRestify\Repositories\ActionLogRepository; public static function related(): array { return [ - 'logs' => MorphToMany::make('actionLogs', 'actionLogs', ActionLogRepository::class), + 'logs' => MorphToMany::make('actionLogs', ActionLogRepository::class), ]; } ``` @@ -334,30 +445,57 @@ performed for posts: "id": "1", "type": "action_logs", "attributes": { - "batch_id": "048686bb-cd22-41a7-a6db-3eba29678d74", "user_id": "1", "name": "Stored", "actionable_type": "App\\Models\\Post", "actionable_id": "1", - "target_type": "App\\Models\\Post", - "target_id": "1", - "model_type": "App\\Models\\Post", - "model_id": "1", - "fields": "", "status": "finished", - "original": "", + "original": [], "changes": [], "exception": "" - }, - "meta": { - "authorizedToShow": true, - "authorizedToStore": true, - "authorizedToUpdate": true, - "authorizedToDelete": true } } ] ``` -Definitely you can use your own `ActionLogRepository` to represent the data returned, maybe you prefer to represent the -user details or something else. +### Custom logs repository + +Definitely you can use your own `ActionLogRepository`. Just ensure you define it into the config: + +```php +// config/restify.php +... +'logs' => [ + 'repository' => MyCustomLogsRepository::class, +], +``` + +## Get available actions + +The frontend which consume your API could check available actions by using exposed endpoint: + +```http request +GET: api/api/restify/posts/actions +``` + +This will answer with a json like: + +```json +{ + "data": { + "name": "Publish Posts Action", + "destructive": false, + "uriKey": "publish-posts-action", + "payload": [] + } +} +``` + +`name` - humanized name of the action + +`destructive` - you may extend the `Binaryk\LaravelRestify\Actions\DestructiveAction` to indicate to the frontend than +this action is destructive (could be used for deletions) + +`uriKey` - is the key of the action, will be used to perform the action + +`payload` - a key / value object indicating required payload defined in the `rules` Action class diff --git a/docs-v2/content/en/api/fields.md b/docs-v2/content/en/api/fields.md index d0e4e33b2..3b149991c 100644 --- a/docs-v2/content/en/api/fields.md +++ b/docs-v2/content/en/api/fields.md @@ -13,7 +13,9 @@ Each Field generally extends the `Binaryk\LaravelRestify\Fields\Field` class fro a fluent API for a variety of mutators, interceptors and validators. To add a field to a repository, we can simply add it to the repository's fields method. Typically, fields may be created -using their static `new` or `make` method. These methods accept the underlying database column as argument: +using their static `new` or `make` method. + +The first argument is always the attribute name, and usually matches the database `column`. ```php @@ -45,6 +47,39 @@ field('email') +### Computed field + +The second optional argument is a callback or invokable, and it represents the displayable value of the field either in `show` or `index` requests. + +```php +field('name', fn() => 'John Doe') +``` + +The field above will always return the `name` value as `John Doe`. The field is still writeable, so you can update or create an entity using it. + +### Readonly field + +If you don't want a field to be writeable you can mark it readonly: + +```php +field('title')->readonly() +``` + +The `readonly` accepts a request as well as you can use: + +```php +field('title')->readonly(fn($request) => $request->user()->isGuest()) +``` + +### Virtual field + +A virtual field, is a field that's [computed](#computed-field) and [readonly](#readonly-field). + +```php +field('name', fn() => "$this->first_name $this->last_name")->readonly() +``` + + ## Authorization The `Field` class provides few methods to authorize certain actions. Each authorization method accept a `Closure` that @@ -240,6 +275,55 @@ Field::new('password')->showRequest(function ($value) { return Hash::make($value); }); ``` + +### Fields actionable + +Sometime storing attributes might require the stored model before saving it. + +For example, say the Post model uses the [media library](https://spatie.be/docs/laravel-medialibrary/v9/introduction) and has the `media` relationship, that's a list of Media files: + +```php +// PostRepository + +public function fields(RestifyRequest $request): array +{ + return [ + field('title'), + + field('files', + fn () => $this->model()->media()->pluck('file_name') + ) + ->action(new AttachPostFileRestifyAction), + ]; +} +``` + +So we have a virtual `files` field (it's not an actual database column) that uses a [computed field](#computed-field) to display the list of Post's files names. The `->action()` call, accept an instance of a class that extends `Binaryk\LaravelRestify\Actions\Action`: + +```php +class AttachPostFileRestifyAction extends Action +{ + public function handle(RestifyRequest $request, Post $post): void + { + $post->addMediaFromRequest('file'); + } +} +``` + +The action gets the `$request` and the current `$post` model. Say the frontend has to create a post with a file: + +```javascript +const data = new FormData; +data.append('file', blobFile); +data.append('title', 'Post title'); + +axios.post(`api/restify/posts`, data); +``` + +In a single request we're able to create the post and attach file using media library, otherwise it would involve 2 separate requests (post creation and file attaching). + +Actionable fields handle [store](/repositories#store-request), put, [bulk store](/repositories#store-bulk-flow) and bulk update requests. + ## Fallbacks ### Default Stored Value diff --git a/docs-v2/content/en/search/advanced-filters.md b/docs-v2/content/en/search/advanced-filters.md index d37fc9889..b80f061c9 100644 --- a/docs-v2/content/en/search/advanced-filters.md +++ b/docs-v2/content/en/search/advanced-filters.md @@ -38,6 +38,8 @@ class ReadyPostsFilter extends AdvancedFilter }; ``` +### Register filter + Then add the filter to the repository `filters` method: ```php diff --git a/src/Actions/Action.php b/src/Actions/Action.php index 84c4607c5..0c455f4f1 100644 --- a/src/Actions/Action.php +++ b/src/Actions/Action.php @@ -22,7 +22,7 @@ /** * Class Action - * @method JsonResponse handle(Request $request, Model|Collection $models) + * @method JsonResponse handle(Request $request, Model|Collection $models, ?int $row) * @package Binaryk\LaravelRestify\Actions */ abstract class Action implements JsonSerializable @@ -107,9 +107,20 @@ public function canRun(Closure $callback) /** * Get the payload available on the action. * + * @deprecated Use rules instead * @return array */ public function payload(): array + { + return $this->rules(); + } + + /** + * Validation rules to be applied before the action is called. + * + * @return array + */ + public function rules(): array { return []; } diff --git a/src/Fields/Field.php b/src/Fields/Field.php index 98c55dc8e..efd7ce825 100644 --- a/src/Fields/Field.php +++ b/src/Fields/Field.php @@ -148,10 +148,6 @@ public function __construct($attribute, callable|Closure $resolveCallback = null } else { $this->attribute = $attribute ?? str_replace(' ', '_', Str::lower($attribute)); } - - if (is_callable($resolveCallback)) { - $this->readonly(); - } } public function indexCallback(callable|Closure $callback) diff --git a/src/Fields/FieldCollection.php b/src/Fields/FieldCollection.php index 83ae188ad..baf964a51 100644 --- a/src/Fields/FieldCollection.php +++ b/src/Fields/FieldCollection.php @@ -78,10 +78,10 @@ public function forStore(RestifyRequest $request, $repository): self })->values(); } - public function withActions(RestifyRequest $request, $repository): self + public function withActions(RestifyRequest $request, $repository, $row = null): self { return $this - ->inRequest($request) + ->inRequest($request, $row) ->filter(fn (Field $field) => $field->isActionable()) ->values(); } @@ -154,10 +154,14 @@ public function findFieldByAttribute($attribute, $default = null) return null; } - public function inRequest(RestifyRequest $request): self + public function inRequest(RestifyRequest $request, $row = null): self { return $this - ->filter(fn (Field $field) => $request->has($field->attribute) || $request->hasFile($field->attribute)) + ->filter( + fn (Field $field) => + $request->hasAny($field->attribute, $row.'.'.$field->attribute) + || $request->hasFile($field->attribute) + ) ->values(); } } diff --git a/src/Repositories/Repository.php b/src/Repositories/Repository.php index 5870de5e0..017a76132 100644 --- a/src/Repositories/Repository.php +++ b/src/Repositories/Repository.php @@ -672,6 +672,7 @@ public function storeBulk(RepositoryStoreBulkRequest $request) $this->resource, $fields = $this->collectFields($request) ->forStoreBulk($request, $this) + ->withoutActions($request, $this) ->authorizedUpdateBulk($request), $row ); @@ -680,6 +681,13 @@ public function storeBulk(RepositoryStoreBulkRequest $request) $fields->each(fn (Field $field) => $field->invokeAfter($request, $this->resource)); + $this + ->collectFields($request) + ->forStoreBulk($request, $this) + ->withActions($request, $this, $row) + ->authorizedUpdateBulk($request) + ->each(fn (Field $field) => $field->actionHandler->handle($request, $this->resource, $row)); + return $this->resource; }); }); @@ -764,12 +772,20 @@ public function updateBulk(RestifyRequest $request, $repositoryId, int $row) { $fields = $this->collectFields($request) ->forUpdateBulk($request, $this) + ->withoutActions($request, $this) ->authorizedUpdateBulk($request); static::fillBulkFields($request, $this->resource, $fields, $row); $this->resource->save(); + $this + ->collectFields($request) + ->forUpdateBulk($request, $this) + ->withActions($request, $this, $row) + ->authorizedUpdateBulk($request) + ->each(fn (Field $field) => $field->actionHandler->handle($request, $this->resource, $row)); + static::updatedBulk($this->resource, $request); return response()->json(); diff --git a/src/helpers.php b/src/helpers.php index 7d64b3c7c..4fafa4032 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -30,7 +30,7 @@ function data(mixed $data = [], int $status = 200, array $headers = [], $options if (! function_exists('ok')) { function ok() { - return response('', 204); + return response('', 204)->json([], 204); } } diff --git a/tests/Actions/FieldActionTest.php b/tests/Actions/FieldActionTest.php index 913e9b00f..99d7d1499 100644 --- a/tests/Actions/FieldActionTest.php +++ b/tests/Actions/FieldActionTest.php @@ -49,4 +49,110 @@ public function handle(RestifyRequest $request, Post $post) ->etc() ); } + + /** @test */ + public function can_use_actionable_field_on_bulk_store(): void + { + $action = new class extends Action { + public bool $showOnShow = true; + + public function handle(RestifyRequest $request, Post $post, int $row) + { + $description = data_get($request[$row], 'description'); + + $post->update([ + 'description' => 'Actionable ' . $description, + ]); + } + }; + + PostRepository::partialMock() + ->shouldReceive('fieldsForStoreBulk') + ->andreturn([ + Field::new('title'), + + Field::new('description')->action($action), + ]); + + $this + ->withoutExceptionHandling() + ->postJson(PostRepository::to('bulk'), [ + [ + 'title' => $title1 = 'First title', + 'description' => 'first description', + ], + [ + 'title' => $title2 = 'Second title', + 'description' => 'second description', + ], + ]) + ->assertJson( + fn (AssertableJson $json) => $json + ->where('data.0.title', $title1) + ->where('data.0.description', 'Actionable first description') + ->where('data.1.title', $title2) + ->where('data.1.description', 'Actionable second description') + ->etc() + ); + } + + /** @test */ + public function can_use_actionable_field_on_bulk_update(): void + { + $action = new class extends Action { + public bool $showOnShow = true; + + public function handle(RestifyRequest $request, Post $post, int $row) + { + $description = data_get($request[$row], 'description'); + + $post->update([ + 'description' => 'Actionable ' . $description, + ]); + } + }; + + PostRepository::partialMock() + ->shouldReceive('fieldsForUpdateBulk') + ->andreturn([ + Field::new('title'), + + Field::new('description')->action($action), + ]); + + $postId1 = $this + ->withoutExceptionHandling() + ->postJson(PostRepository::to(), [ + 'title' => 'First title', + ])->json('data.id'); + + $postId2 = $this + ->withoutExceptionHandling() + ->postJson(PostRepository::to(), [ + 'title' => 'Second title', + ])->json('data.id'); + + $this + ->withoutExceptionHandling() + ->postJson(PostRepository::to('bulk/update'), [ + [ + 'id' => $postId1, + 'description' => 'first description', + ], + [ + 'id' => $postId2, + 'description' => 'second description', + ], + ])->assertOk(); + + $this->assertSame( + 'Actionable first description', + Post::find($postId1)->description + ); + + $this->assertSame( + 'Actionable second description', + Post::find($postId2)->description + ); + } } diff --git a/tests/Actions/PerformActionControllerTest.php b/tests/Actions/PerformActionControllerTest.php index aabd9cc7a..bfdb1720b 100644 --- a/tests/Actions/PerformActionControllerTest.php +++ b/tests/Actions/PerformActionControllerTest.php @@ -86,7 +86,7 @@ public function test_show_action_not_need_repositories() $this->assertEquals(1, ActivateAction::$applied[0]->id); } - public function test_could_perform_standalone_action() + public function test_could_perform_standalone_action(): void { $this->postJson('users/action?action='.(new DisableProfileAction())->uriKey()) ->assertSuccessful() diff --git a/tests/Fixtures/Post/PostRepository.php b/tests/Fixtures/Post/PostRepository.php index 684b94d64..766fe91eb 100644 --- a/tests/Fixtures/Post/PostRepository.php +++ b/tests/Fixtures/Post/PostRepository.php @@ -36,13 +36,13 @@ public static function indexQuery(RestifyRequest $request, $query) public function fields(RestifyRequest $request): array { return [ - Field::new('user_id'), + field('user_id'), - Field::new('title')->storingRules('required')->messages([ + field('title')->storingRules('required')->messages([ 'required' => 'This field is required', ]), - Field::new('description')->storingRules('required')->messages([ + field('description')->storingRules('required')->messages([ 'required' => 'Description field is required', ]), ]; @@ -51,9 +51,9 @@ public function fields(RestifyRequest $request): array public function fieldsForStore(RestifyRequest $request): array { return [ - Field::new('user_id'), + field('user_id'), - Field::new('title')->storingRules('required')->messages([ + field('title')->storingRules('required')->messages([ 'required' => 'This field is required', ]), ]; @@ -62,22 +62,22 @@ public function fieldsForStore(RestifyRequest $request): array public function fieldsForStoreBulk(RestifyRequest $request) { return [ - Field::new('title')->storeBulkRules('required')->messages([ + field('title')->storeBulkRules('required')->messages([ 'required' => 'This field is required', ]), - Field::new('user_id'), + field('user_id'), ]; } public function fieldsForUpdateBulk(RestifyRequest $request) { return [ - Field::new('title')->updateBulkRules('required')->messages([ + field('title')->updateBulkRules('required')->messages([ 'required' => 'This field is required', ]), - Field::new('user_id'), + field('user_id'), ]; } diff --git a/tests/Fixtures/Post/PublishPostAction.php b/tests/Fixtures/Post/PublishPostAction.php index 6b335a48e..160b20d76 100644 --- a/tests/Fixtures/Post/PublishPostAction.php +++ b/tests/Fixtures/Post/PublishPostAction.php @@ -17,6 +17,7 @@ public static function indexQuery(RestifyRequest $request, $query) $query->whereNotNull('published_at'); } + public function handle(ActionRequest $request, Collection $models): JsonResponse { static::$applied[] = $models;