fix(syncedinstance): Prevent same-instance syncs and sanitize post update payloads; update tests and docs

This commit is contained in:
2025-10-11 06:16:44 +00:00
parent 00dd0c69a5
commit 2bb86552e2
9 changed files with 582 additions and 49 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@apiclient.xyz/ghost',
version: '2.2.0',
version: '2.2.1',
description: 'An unofficial Ghost CMS API package enabling content and admin functionality for managing posts.'
}

View File

@@ -118,9 +118,34 @@ export class Post {
return this.postData;
}
public async update(postData: IPost): Promise<Post> {
public async update(postData: Partial<IPost>): Promise<Post> {
try {
const updatedPostData = await this.ghostInstanceRef.adminApi.posts.edit(postData);
// Only send fields that should be updated, not the entire post object with nested relations
const updatePayload: any = {
id: this.postData.id,
updated_at: this.postData.updated_at, // Required for conflict detection
...postData
};
// Remove read-only or computed fields that shouldn't be sent
delete updatePayload.uuid;
delete updatePayload.comment_id;
delete updatePayload.url;
delete updatePayload.excerpt;
delete updatePayload.reading_time;
delete updatePayload.created_at; // Don't send created_at in updates
delete updatePayload.primary_author;
delete updatePayload.primary_tag;
delete updatePayload.count;
delete updatePayload.email;
delete updatePayload.newsletter;
// Remove nested objects if they're not being updated
if (!postData.authors) delete updatePayload.authors;
if (!postData.tags) delete updatePayload.tags;
if (!postData.tiers) delete updatePayload.tiers;
const updatedPostData = await this.ghostInstanceRef.adminApi.posts.edit(updatePayload);
this.postData = updatedPostData;
return this;
} catch (error) {

View File

@@ -50,6 +50,20 @@ export class SyncedInstance {
private syncHistory: ISyncReport[];
constructor(sourceGhost: Ghost, targetGhosts: Ghost[]) {
// Validate that no target instance is the same as the source instance
const sourceUrl = sourceGhost.options.baseUrl.replace(/\/$/, '').toLowerCase();
for (const targetGhost of targetGhosts) {
const targetUrl = targetGhost.options.baseUrl.replace(/\/$/, '').toLowerCase();
if (sourceUrl === targetUrl) {
throw new Error(
`Cannot sync to the same instance. Source and target both point to: ${sourceUrl}. ` +
`This would create a circular sync and cause excessive API calls.`
);
}
}
this.sourceGhost = sourceGhost;
this.targetGhosts = targetGhosts;
this.syncMappings = new Map();
@@ -125,6 +139,7 @@ export class SyncedInstance {
if (!optionsArg?.dryRun) {
await targetTag.update({
name: sourceTag.name,
slug: sourceTag.slug,
description: sourceTag.description,
feature_image: sourceTag.feature_image,
visibility: sourceTag.visibility,