Massively creating resources (#3152)

* Bulk storing request & accesing policy service

* Transform hardcoded action to self-property

* Testing, support for multiple resources

* Uncomment, response(200)

* Change authorize to isAdmin() property
This commit is contained in:
Benjamin Beganović 2019-12-17 11:58:23 +01:00 committed by David Bomba
parent b6cb0172c3
commit da49880733
6 changed files with 281 additions and 45 deletions

View File

@ -14,6 +14,7 @@ namespace App\Http\Controllers;
use App\DataMapper\ClientSettings;
use App\Factory\ClientFactory;
use App\Filters\ClientFilters;
use App\Http\Requests\Client\BulkClientRequest;
use App\Http\Requests\Client\CreateClientRequest;
use App\Http\Requests\Client\DestroyClientRequest;
use App\Http\Requests\Client\EditClientRequest;
@ -23,6 +24,7 @@ use App\Http\Requests\Client\UpdateClientRequest;
use App\Jobs\Client\StoreClient;
use App\Jobs\Client\UpdateClient;
use App\Jobs\Entity\ActionEntity;
use App\Jobs\Util\ProcessBulk;
use App\Jobs\Util\UploadAvatar;
use App\Models\Client;
use App\Models\ClientContact;
@ -32,6 +34,7 @@ use App\Models\Size;
use App\Repositories\BaseRepository;
use App\Repositories\ClientRepository;
use App\Transformers\ClientTransformer;
use App\Utils\Traits\BulkOptions;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\Uploadable;
use Illuminate\Http\Request;
@ -47,7 +50,8 @@ class ClientController extends BaseController
{
use MakesHash;
use Uploadable;
use BulkOptions;
protected $entity_type = Client::class;
protected $entity_transformer = ClientTransformer::class;
@ -98,7 +102,7 @@ class ClientController extends BaseController
* ),
* @OA\Response(
* response="default",
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
@ -107,9 +111,9 @@ class ClientController extends BaseController
*/
public function index(ClientFilters $filters)
{
$clients = Client::filter($filters);
return $this->listResponse($clients);
}
@ -157,7 +161,7 @@ class ClientController extends BaseController
*
* ),
* @OA\Response(
* response="default",
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
@ -167,7 +171,7 @@ class ClientController extends BaseController
public function show(ShowClientRequest $request, Client $client)
{
return $this->itemResponse($client);
return $this->itemResponse($client);
}
@ -177,7 +181,7 @@ class ClientController extends BaseController
* @param int $id
* @return \Illuminate\Http\Response
*
*
*
* @OA\Get(
* path="/api/v1/clients/{id}/edit",
* operationId="editClient",
@ -214,7 +218,7 @@ class ClientController extends BaseController
*
* ),
* @OA\Response(
* response="default",
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
@ -235,8 +239,8 @@ class ClientController extends BaseController
* @param App\Models\Client $client
* @return \Illuminate\Http\Response
*
*
*
*
*
* @OA\Put(
* path="/api/v1/clients/{id}",
* operationId="updateClient",
@ -273,7 +277,7 @@ class ClientController extends BaseController
*
* ),
* @OA\Response(
* response="default",
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
@ -296,8 +300,8 @@ class ClientController extends BaseController
*
* @return \Illuminate\Http\Response
*
*
*
*
*
* @OA\Get(
* path="/api/v1/clients/create",
* operationId="getClientsCreate",
@ -323,7 +327,7 @@ class ClientController extends BaseController
*
* ),
* @OA\Response(
* response="default",
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
@ -332,7 +336,7 @@ class ClientController extends BaseController
*/
public function create(CreateClientRequest $request)
{
$client = ClientFactory::create(auth()->user()->company()->id, auth()->user()->id);
return $this->itemResponse($client);
@ -372,7 +376,7 @@ class ClientController extends BaseController
*
* ),
* @OA\Response(
* response="default",
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
@ -398,7 +402,7 @@ class ClientController extends BaseController
* @param int $id
* @return \Illuminate\Http\Response
*
*
*
* @OA\Delete(
* path="/api/v1/clients/{id}",
* operationId="deleteClient",
@ -434,7 +438,7 @@ class ClientController extends BaseController
*
* ),
* @OA\Response(
* response="default",
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
@ -451,10 +455,11 @@ class ClientController extends BaseController
/**
* Perform bulk actions on the list view
*
* @return Collection
*
*
* @param BulkClientRequest $request
* @return \Illuminate\Http\Response
*
*
* @OA\Post(
* path="/api/v1/clients/bulk",
* operationId="bulkClients",
@ -492,39 +497,50 @@ class ClientController extends BaseController
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* ),
* @OA\Response(
* response="default",
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*
*/
public function bulk()
public function bulk(BulkClientRequest $request)
{
$action = $request->action;
$ids = [];
$action = request()->input('action');
$ids = request()->input('ids');
if ($request->action !== self::$STORE_METHOD) {
$clients = Client::withTrashed()->find($this->transformKeys($ids));
$clients->each(function ($client, $key) use($action){
$ids = $request->ids;
if(auth()->user()->can('edit', $client))
$this->client_repo->{$action}($client);
$clients = Client::withTrashed()->find($this->transformKeys($ids));
});
$clients->each(function ($client, $key) use ($request, $action) {
if (auth()->user()->can($request->action, $client))
$this->client_repo->{$action}($client);
});
}
if($request->action == self::$STORE_METHOD) {
/** Small hunks of data (originally 100) */
$chunks = array_chunk($request->clients, self::$CHUNK_SIZE);
foreach($chunks as $data) {
dispatch(new ProcessBulk($data, $this->client_repo, self::$STORE_METHOD))->onQueue(self::$DEFAULT_QUEUE);
}
}
return $this->listResponse(Client::withTrashed()->whereIn('id', $this->transformKeys($ids)));
}
/**
* Returns a client statement
*
*
* @return [type] [description]
*/
public function statement()
@ -532,6 +548,4 @@ class ClientController extends BaseController
//todo
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Http\Requests\Client;
use App\Utils\Traits\BulkOptions;
use Illuminate\Foundation\Http\FormRequest;
use App\Models\Client;
class BulkClientRequest extends FormRequest
{
use BulkOptions;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
if (!$this->has('action')) {
return false;
}
if (!in_array($this->action, $this->getBulkOptions(), true)) {
return false;
}
return auth()->user()->can(auth()->user()->isAdmin(), Client::class);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$rules = $this->getGlobalRules();
/** We don't require IDs on bulk storing. */
if ($this->action !== self::$STORE_METHOD) {
$rules['ids'] = ['required'];
}
return $rules;
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Jobs\Util;
use App\Utils\Traits\BulkOptions;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessBulk implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, BulkOptions;
/**
* Repository for target resource.
*/
protected $repo;
/**
* Method aka 'action' to process.
*
* @var string
*/
protected $method;
/**
* Chunks of data to process.
*
* @var array
*/
private $data;
/**
* Create a new job instance.
*
* @param array $data
* @param $repo
* @param string $method
*/
public function __construct(array $data, $repo, string $method)
{
$this->repo = $repo;
$this->method = $method;
$this->data = $data;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
foreach($this->data as $resource) {
$this->repo->{$this->method}($resource);
}
}
}

View File

@ -11,6 +11,7 @@
namespace App\Repositories;
use App\Factory\ClientFactory;
use App\Models\Client;
use App\Repositories\ClientContactRepository;
use App\Utils\Traits\GeneratesCounter;
@ -71,14 +72,26 @@ class ClientRepository extends BaseRepository
if(isset($data['contacts']))
$contacts = $this->contact_repo->save($data['contacts'], $client);
if(empty($data['name']))
$data['name'] = $client->present()->name();
return $client;
}
/**
* Store clients in bulk.
*
* @param array $client
* @return Client|null
*/
public function create($client): ?Client
{
return $this->save(
$client, ClientFactory::create(auth()->user()->company()->id, auth()->user()->id)
);
}
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Utils\Traits;
trait BulkOptions
{
/**
* Store method in requests.
*
* @var string
*/
public static $STORE_METHOD = 'create';
/**
* Default chunk size.
*
* @var int
*/
public static $CHUNK_SIZE = 100;
/**
* Default queue for bulk processing.
*
* @var string
*/
public static $DEFAULT_QUEUE = 'bulk_processing';
/**
* Available bulk options - used in requests (eg. BulkClientRequests)
*
* @return array
* @var array
*/
public function getBulkOptions()
{
return [
'create', 'edit', 'view',
];
}
/**
* Shared rules for bulk requests.
*
* @return array
* @var array
*/
public function getGlobalRules()
{
return [
'action' => ['required'],
];
}
}

View File

@ -65,7 +65,7 @@ class ClientTest extends TestCase
$acc = $response->json();
$account = Account::find($this->decodePrimaryKey($acc['data'][0]['account']['id']));
$account = Account::find($this->decodePrimaryKey($acc['data'][0]['account']['id']));
$token = $account->default_company->tokens->first()->token;
@ -102,13 +102,13 @@ class ClientTest extends TestCase
$acc = $response->json();
$account = Account::find($this->decodePrimaryKey($acc['data'][0]['account']['id']));
$account = Account::find($this->decodePrimaryKey($acc['data'][0]['account']['id']));
$company_token = $account->default_company->tokens()->first();
$token = $company_token->token;
$company = $company_token->company;
$user = $company_token->user;
//$company_user = $company->company_users()->first();
@ -244,7 +244,7 @@ class ClientTest extends TestCase
$this->assertNotNull($client);
/* Make sure we have a valid settings object*/
$this->assertEquals($client->getSetting('timezone_id'), 1);
$this->assertEquals($client->getSetting('timezone_id'), 1);
/* Make sure we are harvesting valid data */
$this->assertEquals($client->timezone()->name, 'Pacific/Midway');
@ -253,4 +253,51 @@ class ClientTest extends TestCase
$this->assertEquals($client->contacts->count(), 3);
}
/** @test */
public function testMassivelyCreatingClients()
{
$data = [
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'name' => $this->faker->company,
'email' => $this->faker->unique()->safeEmail,
'password' => 'ALongAndBrilliantPassword123',
'_token' => csrf_token(),
'privacy_policy' => 1,
'terms_of_service' => 1
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
])->post('/api/v1/signup?include=account', $data);
$response->assertStatus(200);
$acc = $response->json();
$account = Account::find($this->decodePrimaryKey($acc['data'][0]['account']['id']));
$token = $account->default_company->tokens->first()->token;
$body = [
'action' => 'create',
'clients' => [
['name' => $this->faker->firstName, 'website' => 'my-awesome-website-1.com'],
['name' => $this->faker->firstName, 'website' => 'my-awesome-website-2.com'],
],
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $token,
])->post(route('clients.bulk'), $body);
$response->assertStatus(200);
$first_record = Client::where('website', 'my-awesome-website-1.com')->first();
$second_record = Client::where('website', 'my-awesome-website-2.com')->first();
$this->assertNotNull($first_record);
$this->assertNotNull($second_record);
}
}