[MTM] Update existing draft articles instead of erroring on a repeat Micropub post #139
4 changed files with 92 additions and 2 deletions
Update existing draft articles instead of erroring on a repeat Micropub post
If a Micropub h-entry post's title matches an existing article that's still a draft, update that article in place rather than trying to insert a duplicate. If it matches one that's already published, reject the request with a clear error instead of silently colliding. Also set includeTrashed on Article's slug config as a safety net: this model soft-deletes, and Sluggable's uniqueness check ignores trashed rows by default, so a previously-deleted article's title could crash new inserts with a raw unique constraint violation (this is exactly what surfaced in Flare as a UniqueConstraintViolationException on articles_titleurl_unique once the prior swallowed-exception fix shipped). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
commit
eb35a0aa2d
|
|
@ -40,6 +40,7 @@ class Article extends Model
|
||||||
return [
|
return [
|
||||||
'titleurl' => [
|
'titleurl' => [
|
||||||
'source' => 'title',
|
'source' => 'title',
|
||||||
|
'includeTrashed' => true,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,29 @@ use App\Models\Article;
|
||||||
|
|
||||||
class ArticleService
|
class ArticleService
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @throws \InvalidArgumentException if a published article already has this title
|
||||||
|
*/
|
||||||
public function create(array $data): Article
|
public function create(array $data): Article
|
||||||
{
|
{
|
||||||
return Article::create([
|
$attributes = [
|
||||||
'title' => $data['name'],
|
'title' => $data['name'],
|
||||||
'main' => $data['content'],
|
'main' => $data['content'],
|
||||||
'published' => ($data['post-status'] ?? null) !== 'draft',
|
'published' => ($data['post-status'] ?? null) !== 'draft',
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
$existing = Article::where('title', $data['name'])->first();
|
||||||
|
|
||||||
|
if ($existing !== null) {
|
||||||
|
if ($existing->published) {
|
||||||
|
throw new \InvalidArgumentException("An article titled \"{$data['name']}\" has already been published");
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing->update($attributes);
|
||||||
|
|
||||||
|
return $existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Article::create($attributes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use App\Exceptions\MicropubHandlerException;
|
||||||
use App\Jobs\SendWebMentions;
|
use App\Jobs\SendWebMentions;
|
||||||
use App\Jobs\SyndicateNoteToBluesky;
|
use App\Jobs\SyndicateNoteToBluesky;
|
||||||
use App\Jobs\SyndicateNoteToMastodon;
|
use App\Jobs\SyndicateNoteToMastodon;
|
||||||
|
use App\Models\Article;
|
||||||
use App\Models\Media;
|
use App\Models\Media;
|
||||||
use App\Models\Note;
|
use App\Models\Note;
|
||||||
use App\Models\Place;
|
use App\Models\Place;
|
||||||
|
|
@ -911,4 +912,64 @@ class MicropubControllerTest extends TestCase
|
||||||
'published' => false,
|
'published' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function micropub_client_api_request_updates_an_existing_draft_article_with_the_same_name(): void
|
||||||
|
{
|
||||||
|
$draft = Article::create([
|
||||||
|
'title' => 'WireGuard',
|
||||||
|
'main' => 'Early draft content',
|
||||||
|
'published' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->postJson(
|
||||||
|
'/api/post',
|
||||||
|
[
|
||||||
|
'type' => ['h-entry'],
|
||||||
|
'properties' => [
|
||||||
|
'name' => ['WireGuard'],
|
||||||
|
'content' => ['Finished content'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
['HTTP_Authorization' => 'Bearer '.$this->getToken()]
|
||||||
|
);
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertJson(['response' => 'created'])
|
||||||
|
->assertStatus(201);
|
||||||
|
$this->assertSame(1, Article::where('title', 'WireGuard')->count());
|
||||||
|
$this->assertDatabaseHas('articles', [
|
||||||
|
'id' => $draft->id,
|
||||||
|
'title' => 'WireGuard',
|
||||||
|
'main' => 'Finished content',
|
||||||
|
'published' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function micropub_client_api_request_errors_when_an_article_with_the_same_name_is_already_published(): void
|
||||||
|
{
|
||||||
|
Article::create([
|
||||||
|
'title' => 'WireGuard',
|
||||||
|
'main' => 'Published content',
|
||||||
|
'published' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->postJson(
|
||||||
|
'/api/post',
|
||||||
|
[
|
||||||
|
'type' => ['h-entry'],
|
||||||
|
'properties' => [
|
||||||
|
'name' => ['WireGuard'],
|
||||||
|
'content' => ['Some other content'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
['HTTP_Authorization' => 'Bearer '.$this->getToken()]
|
||||||
|
);
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertJson(['error' => 'invalid_request'])
|
||||||
|
->assertStatus(400);
|
||||||
|
$this->assertSame(1, Article::where('title', 'WireGuard')->count());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,17 @@ class ArticlesTest extends TestCase
|
||||||
$this->assertEquals(config('app.url').$article->link, $article->uri);
|
$this->assertEquals(config('app.url').$article->link, $article->uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function slug_is_suffixed_when_a_trashed_article_already_used_it(): void
|
||||||
|
{
|
||||||
|
$original = Article::create(['title' => 'My Title', 'main' => 'Content']);
|
||||||
|
$original->delete();
|
||||||
|
|
||||||
|
$newArticle = Article::create(['title' => 'My Title', 'main' => 'Other content']);
|
||||||
|
|
||||||
|
$this->assertEquals('my-title-2', $newArticle->titleurl);
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function date_scope_returns_expected_articles(): void
|
public function date_scope_returns_expected_articles(): void
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue