Bring post platform meta (aspect ratio) to parity across API and MCP
PR #82 made `meta.aspect_ratio` crop Facebook (and already Instagram) feed images at publish time, but the API and MCP surfaces only half-supported it: you couldn't set meta at creation, the value wasn't validated, and responses never returned it. This closes those gaps. - New `AspectRatio` enum is the single source of truth for the allowed ratios (1:1, 4:5, 16:9, original). App/API/MCP requests now validate via `Rule::enum(AspectRatio::class)` — an invalid ratio is rejected everywhere instead of silently center-cropping to square. - API `StorePostRequest` and MCP `CreatePostTool` now accept `platforms.*.meta`; `CreatePost` persists it. MCP create documents `meta` in its schema. - `Api\PostPlatformResource` now exposes `meta`, so API and MCP responses return the aspect_ratio (and other per-platform meta) a client set.
This commit is contained in:
parent
693e9c9b40
commit
ce6ee04883
10 changed files with 125 additions and 1 deletions
|
|
@ -62,6 +62,10 @@ public static function execute(Workspace $workspace, User $user, array $data): P
|
|||
$updates['content_type'] = $contentType;
|
||||
}
|
||||
|
||||
if (($meta = data_get($platformData, 'meta')) !== null) {
|
||||
$updates['meta'] = $meta;
|
||||
}
|
||||
|
||||
$post->postPlatforms()
|
||||
->where('social_account_id', $accountId)
|
||||
->update($updates);
|
||||
|
|
|
|||
13
app/Enums/PostPlatform/AspectRatio.php
Normal file
13
app/Enums/PostPlatform/AspectRatio.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\PostPlatform;
|
||||
|
||||
enum AspectRatio: string
|
||||
{
|
||||
case Square = '1:1';
|
||||
case Portrait = '4:5';
|
||||
case Landscape = '16:9';
|
||||
case Original = 'original';
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace App\Http\Requests\Api\Post;
|
||||
|
||||
use App\Enums\PostPlatform\AspectRatio;
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Models\SocialAccount;
|
||||
|
|
@ -49,6 +50,8 @@ public function rules(): array
|
|||
Rule::in(array_column(ContentType::cases(), 'value')),
|
||||
new ContentTypeMatchesPlatform,
|
||||
],
|
||||
'platforms.*.meta' => ['sometimes', 'nullable', 'array'],
|
||||
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::enum(AspectRatio::class)],
|
||||
'scheduled_at' => ['nullable', 'date', 'after:now'],
|
||||
'label_ids' => ['sometimes', 'array'],
|
||||
'label_ids.*' => [
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
namespace App\Http\Requests\Api\Post;
|
||||
|
||||
use App\Enums\Post\Status;
|
||||
use App\Enums\PostPlatform\AspectRatio;
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Models\Post;
|
||||
|
|
@ -51,6 +52,7 @@ public function rules(): array
|
|||
new ContentTypeMatchesPostPlatform,
|
||||
],
|
||||
'platforms.*.meta' => ['nullable', 'array'],
|
||||
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::enum(AspectRatio::class)],
|
||||
'scheduled_at' => [
|
||||
'nullable',
|
||||
'date',
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use App\Enums\Media\Source;
|
||||
use App\Enums\Post\Status;
|
||||
use App\Enums\PostPlatform\AspectRatio;
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Rules\ContentFitsPlatformLimits;
|
||||
|
|
@ -70,7 +71,7 @@ public function rules(): array
|
|||
Rule::when($enforcesMediaCompatibility, [new ContentTypeCompatibleWithMedia]),
|
||||
],
|
||||
'platforms.*.meta' => ['nullable', 'array'],
|
||||
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::in(['1:1', '4:5', '16:9', 'original'])],
|
||||
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::enum(AspectRatio::class)],
|
||||
'platforms.*.meta.privacy_level' => ['sometimes', 'nullable', 'string', Rule::in(['PUBLIC_TO_EVERYONE', 'MUTUAL_FOLLOW_FRIENDS', 'FOLLOWER_OF_CREATOR', 'SELF_ONLY'])],
|
||||
'platforms.*.meta.auto_add_music' => ['sometimes', 'boolean'],
|
||||
'platforms.*.meta.allow_comments' => ['sometimes', 'boolean'],
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public function toArray(Request $request): array
|
|||
'id' => $this->id,
|
||||
'platform' => $this->platform?->value,
|
||||
'content_type' => $this->content_type?->value,
|
||||
'meta' => $this->meta,
|
||||
'status' => $this->status?->value,
|
||||
'enabled' => $this->enabled,
|
||||
'platform_url' => $this->platform_url,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
namespace App\Mcp\Tools\Post;
|
||||
|
||||
use App\Actions\Post\CreatePost;
|
||||
use App\Enums\PostPlatform\AspectRatio;
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Http\Resources\Api\PostResource;
|
||||
use App\Rules\ContentTypeMatchesPlatform;
|
||||
|
|
@ -37,6 +38,8 @@ public function handle(Request $request): ResponseFactory
|
|||
->where('is_active', true),
|
||||
],
|
||||
'platforms.*.content_type' => ['required', 'string', Rule::in(array_column(ContentType::cases(), 'value')), new ContentTypeMatchesPlatform],
|
||||
'platforms.*.meta' => ['sometimes', 'array'],
|
||||
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::enum(AspectRatio::class)],
|
||||
]);
|
||||
|
||||
$post = CreatePost::execute($workspace, $request->user(), $validated);
|
||||
|
|
@ -58,6 +61,7 @@ public function schema(JsonSchema $schema): array
|
|||
->items($schema->object(fn ($p) => [
|
||||
'social_account_id' => $p->string()->required()->description('UUID of the connected social account.'),
|
||||
'content_type' => $p->string()->required()->description('Format for this platform (e.g. linkedin_post, x_post, instagram_feed).'),
|
||||
'meta' => $p->object()->description('Per-platform metadata (e.g. aspect_ratio: 1:1|4:5|16:9|original for Instagram/Facebook feed images).'),
|
||||
]))
|
||||
->description('Platforms to publish on. Accounts not listed remain available but disabled.'),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
use App\Actions\Post\UpdatePost;
|
||||
use App\Enums\Post\Action as PostAction;
|
||||
use App\Enums\Post\Status;
|
||||
use App\Enums\PostPlatform\AspectRatio;
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Http\Resources\Api\PostResource;
|
||||
use App\Models\Post;
|
||||
|
|
@ -49,6 +50,7 @@ public function handle(Request $request): Response|ResponseFactory
|
|||
],
|
||||
'platforms.*.content_type' => ['sometimes', 'string', Rule::in(array_column(ContentType::cases(), 'value')), new ContentTypeMatchesPostPlatform],
|
||||
'platforms.*.meta' => ['sometimes', 'array'],
|
||||
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::enum(AspectRatio::class)],
|
||||
]);
|
||||
|
||||
$payload = collect($validated)->except('post_id')->all();
|
||||
|
|
|
|||
|
|
@ -577,3 +577,50 @@
|
|||
->assertOk()
|
||||
->assertJsonStructure(['id', 'status', 'scheduled_at', 'published_at']);
|
||||
});
|
||||
|
||||
it('creates a post with platform meta (aspect_ratio) and returns it', function () {
|
||||
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
||||
->postJson(route('api.posts.store'), [
|
||||
'platforms' => [
|
||||
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => '4:5']],
|
||||
],
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('platforms.0.meta.aspect_ratio', '4:5');
|
||||
|
||||
$platform = Post::where('workspace_id', $this->workspace->id)->first()
|
||||
->postPlatforms()->where('social_account_id', $this->socialAccount->id)->first();
|
||||
expect($platform->meta['aspect_ratio'])->toBe('4:5');
|
||||
});
|
||||
|
||||
it('rejects creating a post with an invalid aspect_ratio', function () {
|
||||
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
||||
->postJson(route('api.posts.store'), [
|
||||
'platforms' => [
|
||||
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => '3:2']],
|
||||
],
|
||||
])
|
||||
->assertJsonValidationErrors(['platforms.0.meta.aspect_ratio']);
|
||||
});
|
||||
|
||||
it('rejects updating a post with an invalid aspect_ratio', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'status' => PostStatus::Draft,
|
||||
]);
|
||||
$postPlatform = PostPlatform::factory()->linkedin()->create([
|
||||
'post_id' => $post->id,
|
||||
'social_account_id' => $this->socialAccount->id,
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
||||
->putJson(route('api.posts.update', $post), [
|
||||
'status' => 'draft',
|
||||
'platforms' => [
|
||||
['id' => $postPlatform->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => '3:2']],
|
||||
],
|
||||
])
|
||||
->assertJsonValidationErrors(['platforms.0.meta.aspect_ratio']);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -276,3 +276,50 @@
|
|||
|
||||
$response->assertHasErrors();
|
||||
});
|
||||
|
||||
test('create post persists platform meta (aspect_ratio)', function () {
|
||||
$response = TryPostServer::actingAs($this->user)
|
||||
->tool(CreatePostTool::class, [
|
||||
'platforms' => [
|
||||
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => '4:5']],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$platform = Post::where('workspace_id', $this->workspace->id)->first()
|
||||
->postPlatforms()->where('social_account_id', $this->socialAccount->id)->first();
|
||||
expect($platform->meta['aspect_ratio'])->toBe('4:5');
|
||||
});
|
||||
|
||||
test('create post rejects an invalid aspect_ratio', function () {
|
||||
$response = TryPostServer::actingAs($this->user)
|
||||
->tool(CreatePostTool::class, [
|
||||
'platforms' => [
|
||||
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => '3:2']],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertHasErrors();
|
||||
});
|
||||
|
||||
test('update post rejects an invalid aspect_ratio', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
$platform = PostPlatform::factory()->create([
|
||||
'post_id' => $post->id,
|
||||
'social_account_id' => $this->socialAccount->id,
|
||||
]);
|
||||
|
||||
$response = TryPostServer::actingAs($this->user)
|
||||
->tool(UpdatePostTool::class, [
|
||||
'post_id' => $post->id,
|
||||
'platforms' => [
|
||||
['id' => $platform->id, 'meta' => ['aspect_ratio' => '3:2']],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertHasErrors();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue