mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-05-24 02:14:21 -04:00
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:
parent
b6cb0172c3
commit
da49880733
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
47
app/Http/Requests/Client/BulkClientRequest.php
Normal file
47
app/Http/Requests/Client/BulkClientRequest.php
Normal 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;
|
||||
}
|
||||
}
|
60
app/Jobs/Util/ProcessBulk.php
Normal file
60
app/Jobs/Util/ProcessBulk.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
55
app/Utils/Traits/BulkOptions.php
Normal file
55
app/Utils/Traits/BulkOptions.php
Normal 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'],
|
||||
];
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user