feat: add full CRUD Posts API

Adds JSON API for Posts (index/show/store/update/destroy) so an AI
agent can draft posts, read what's due for publishing, and report
back publish/fail status. Unlike the existing read-only Sites/Accounts
APIs, this is full CRUD since the app itself never publishes anything.

- PostController with status/site_id/social_account_id/due filters
- StorePostRequest/UpdatePostRequest validate against Post::TYPES/STATUSES
- PostResource shapes the JSON response
- Route::apiResource('posts', ...) registered with ->names('api.posts')
  to avoid colliding with the existing web posts.index route name
- store() calls ->fresh() after create() so the response reflects the
  DB-level default status ('draft') instead of a stale in-memory null

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Doar Mihai
2026-07-18 17:22:16 +03:00
co-authored by Claude Sonnet 5
parent be8e6e997f
commit e0b8f2f924
6 changed files with 281 additions and 0 deletions
@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StorePostRequest;
use App\Http\Requests\UpdatePostRequest;
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index(Request $request)
{
$query = Post::query();
if ($request->filled('status')) {
$query->where('status', $request->string('status'));
}
if ($request->filled('site_id')) {
$query->where('site_id', $request->integer('site_id'));
}
if ($request->filled('social_account_id')) {
$query->where('social_account_id', $request->integer('social_account_id'));
}
if ($request->boolean('due')) {
$query->where('status', 'scheduled')
->where('scheduled_at', '<=', now());
}
return PostResource::collection($query->orderByDesc('created_at')->get());
}
public function show(Post $post)
{
return new PostResource($post);
}
public function store(StorePostRequest $request)
{
$post = Post::create($request->validated())->fresh();
return (new PostResource($post))->response()->setStatusCode(201);
}
public function update(UpdatePostRequest $request, Post $post)
{
$post->update($request->validated());
return new PostResource($post);
}
public function destroy(Post $post)
{
$post->delete();
return response()->noContent();
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Requests;
use App\Models\Post;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'site_id' => ['required', 'integer', 'exists:sites,id'],
'social_account_id' => ['nullable', 'integer', 'exists:social_accounts,id'],
'type' => ['required', 'string', 'in:' . implode(',', Post::TYPES)],
'title' => ['nullable', 'string', 'max:255'],
'body' => ['required', 'string'],
'status' => ['sometimes', 'string', 'in:' . implode(',', Post::STATUSES)],
'scheduled_at' => ['nullable', 'date'],
];
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Requests;
use App\Models\Post;
use Illuminate\Foundation\Http\FormRequest;
class UpdatePostRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'site_id' => ['sometimes', 'required', 'integer', 'exists:sites,id'],
'social_account_id' => ['nullable', 'integer', 'exists:social_accounts,id'],
'type' => ['sometimes', 'required', 'string', 'in:' . implode(',', Post::TYPES)],
'title' => ['nullable', 'string', 'max:255'],
'body' => ['sometimes', 'required', 'string'],
'status' => ['sometimes', 'string', 'in:' . implode(',', Post::STATUSES)],
'scheduled_at' => ['nullable', 'date'],
];
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'site_id' => $this->site_id,
'social_account_id' => $this->social_account_id,
'type' => $this->type,
'title' => $this->title,
'body' => $this->body,
'status' => $this->status,
'scheduled_at' => $this->scheduled_at,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}