Run batches and import JSON or CSV through the API
Each operation is dispatched as a request of its own, so a batch is held to the same validation and permissions as the calls it contains. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
99
app/Http/Controllers/Api/V1/BatchController.php
Normal file
99
app/Http/Controllers/Api/V1/BatchController.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\AdminContent;
|
||||
use App\Support\Batch\BatchRunner;
|
||||
use App\Support\Batch\Importer;
|
||||
use Closure;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* Several calls in one: a batch of any admin operations, and importing rows
|
||||
* of one kind of record from JSON or CSV.
|
||||
*/
|
||||
class BatchController extends Controller
|
||||
{
|
||||
public function batch(Request $request, BatchRunner $runner): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'operations' => ['required', 'array', 'min:1', 'max:'.BatchRunner::MAX_OPERATIONS],
|
||||
'operations.*.method' => ['required', Rule::in(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'get', 'post', 'put', 'patch', 'delete'])],
|
||||
'operations.*.path' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:500',
|
||||
function (string $attribute, mixed $value, Closure $fail): void {
|
||||
if (! is_string($value) || ! BatchRunner::allows($value)) {
|
||||
$fail(__('rules.batch.path'));
|
||||
}
|
||||
},
|
||||
],
|
||||
'operations.*.body' => ['sometimes', 'nullable', 'array'],
|
||||
'atomic' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$outcome = $runner->run($request, $data['operations'], atomic: $request->boolean('atomic'));
|
||||
|
||||
return $this->answer($outcome);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rows as JSON (`rows`) or a CSV file (`file`). `match` names the field
|
||||
* that finds an existing record to update; `dry_run` validates everything
|
||||
* and keeps nothing.
|
||||
*/
|
||||
public function import(Request $request, string $resource, BatchRunner $runner, Importer $importer): JsonResponse
|
||||
{
|
||||
abort_unless(array_key_exists($resource, AdminContent::WITH_HISTORY), 404);
|
||||
|
||||
$request->validate([
|
||||
'rows' => ['required_without:file', 'array'],
|
||||
'rows.*' => ['array'],
|
||||
'file' => ['required_without:rows', 'file', 'mimes:csv,txt', 'max:2048'],
|
||||
'match' => ['sometimes', 'nullable', 'string', Rule::in(['slug', 'name', 'full_name'])],
|
||||
'atomic' => ['sometimes', 'boolean'],
|
||||
'dry_run' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$rows = $request->hasFile('file')
|
||||
? $importer->rowsFromCsv($request->file('file'))
|
||||
: $request->input('rows');
|
||||
|
||||
$operations = $importer->operations($resource, $rows, $request->input('match'));
|
||||
|
||||
// A dry run is all-or-nothing by nature: it keeps nothing either way.
|
||||
$outcome = $runner->run(
|
||||
$request,
|
||||
$operations,
|
||||
atomic: $request->boolean('atomic', true) || $request->boolean('dry_run'),
|
||||
rollback: $request->boolean('dry_run'),
|
||||
);
|
||||
|
||||
return $this->answer($outcome, ['dry_run' => $request->boolean('dry_run')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{results: array<int, array<string, mixed>>, committed: bool} $outcome
|
||||
* @param array<string, mixed> $meta
|
||||
*/
|
||||
protected function answer(array $outcome, array $meta = []): JsonResponse
|
||||
{
|
||||
$failed = collect($outcome['results'])->where('status', '>=', 400)->count();
|
||||
|
||||
return response()->json([
|
||||
'data' => $outcome['results'],
|
||||
'meta' => [
|
||||
...$meta,
|
||||
'committed' => $outcome['committed'],
|
||||
'succeeded' => count($outcome['results']) - $failed,
|
||||
'failed' => $failed,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user