fix(syncedinstance): Prevent same-instance syncs and sanitize post update payloads; update tests and docs
This commit is contained in:
230
readme.md
230
readme.md
@@ -4,17 +4,51 @@
|
||||
|
||||
A modern, fully-typed API client for Ghost CMS that wraps both the Content and Admin APIs into an elegant, developer-friendly interface. Built with TypeScript, designed for humans.
|
||||
|
||||
## ✨ What Makes This Different?
|
||||
|
||||
Unlike the official Ghost SDK, this library gives you:
|
||||
|
||||
- **One unified client** instead of juggling separate Content and Admin API instances
|
||||
- **Class-based models** with helper methods instead of raw JSON objects
|
||||
- **Built-in JWT generation** so you don't need to handle tokens manually
|
||||
- **Pattern matching** with minimatch for flexible filtering
|
||||
- **Multi-instance sync** for managing content across staging/production environments
|
||||
- **Complete tag support** including tags with zero posts (Content API limitation bypassed)
|
||||
- **Universal runtime support** - works in Node.js, Deno, Bun, and browsers without different packages
|
||||
|
||||
## 🚀 Why This Library?
|
||||
|
||||
- **🎯 TypeScript Native** - Full type safety for all Ghost API operations
|
||||
- **🔥 Dual API Support** - Unified interface for both Content and Admin APIs
|
||||
- **⚡ Modern Async/Await** - No callback hell, just clean promises
|
||||
- **🎯 TypeScript Native** - Full type safety for all Ghost API operations with comprehensive interfaces
|
||||
- **🔥 Dual API Support** - Unified interface for both Content and Admin APIs, seamlessly integrated
|
||||
- **⚡ Modern Async/Await** - No callback hell, just clean promises and elegant async patterns
|
||||
- **🌐 Universal Compatibility** - Native fetch implementation works in Node.js, Deno, Bun, and browsers
|
||||
- **🎨 Elegant API** - Intuitive methods that match your mental model
|
||||
- **🔍 Smart Filtering** - Built-in minimatch support for flexible queries
|
||||
- **🎨 Elegant API** - Intuitive methods that match your mental model, not Ghost's quirks
|
||||
- **🔍 Smart Filtering** - Built-in minimatch support for flexible pattern-based queries
|
||||
- **🏷️ Complete Tag Support** - Fetch ALL tags (including zero-count), filter by visibility (internal/external)
|
||||
- **🔄 Multi-Instance Sync** - Synchronize content across multiple Ghost sites
|
||||
- **💪 Production Ready** - Battle-tested with comprehensive error handling
|
||||
- **🔄 Multi-Instance Sync** - Synchronize content across multiple Ghost sites with built-in safety checks
|
||||
- **📅 ISO 8601 Dates** - All dates are properly formatted ISO 8601 strings with timezone support
|
||||
- **🛡️ Built-in JWT Generation** - Automatic JWT token handling for Admin API authentication
|
||||
- **💪 Production Ready** - Battle-tested with 139+ comprehensive tests across Node.js and Deno
|
||||
|
||||
## 📖 Table of Contents
|
||||
|
||||
- [Installation](#-installation)
|
||||
- [Quick Start](#-quick-start)
|
||||
- [Core API](#-core-api)
|
||||
- [Posts](#-posts)
|
||||
- [Pages](#-pages)
|
||||
- [Tags](#️-tags)
|
||||
- [Authors](#-authors)
|
||||
- [Members](#-members)
|
||||
- [Webhooks](#-webhooks)
|
||||
- [Image Upload](#️-image-upload)
|
||||
- [Multi-Instance Synchronization](#-multi-instance-synchronization)
|
||||
- [Complete Example](#-complete-example)
|
||||
- [Performance & Best Practices](#-performance--best-practices)
|
||||
- [Error Handling](#-error-handling)
|
||||
- [API Reference](#-api-reference)
|
||||
- [TypeScript Support](#-typescript-support)
|
||||
- [Testing](#-testing)
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
@@ -35,15 +69,28 @@ import { Ghost } from '@apiclient.xyz/ghost';
|
||||
|
||||
const ghost = new Ghost({
|
||||
baseUrl: 'https://your-ghost-site.com',
|
||||
contentApiKey: 'your_content_api_key',
|
||||
adminApiKey: 'your_admin_api_key'
|
||||
contentApiKey: 'your_content_api_key', // Optional: only needed for reading
|
||||
adminApiKey: 'your_admin_api_key' // Required for write operations
|
||||
});
|
||||
|
||||
const posts = await ghost.getPosts();
|
||||
// Read posts
|
||||
const posts = await ghost.getPosts({ limit: 10 });
|
||||
posts.forEach(post => console.log(post.getTitle()));
|
||||
|
||||
// Create a post
|
||||
const newPost = await ghost.createPost({
|
||||
title: 'Hello World',
|
||||
html: '<p>My first post!</p>',
|
||||
status: 'published'
|
||||
});
|
||||
|
||||
// Update it
|
||||
await newPost.update({
|
||||
title: 'Hello World - Updated!'
|
||||
});
|
||||
```
|
||||
|
||||
That's it. No complicated setup, no boilerplate. Just pure Ghost API goodness.
|
||||
That's it. No complicated setup, no boilerplate. Just pure Ghost API goodness. 🎉
|
||||
|
||||
## 📚 Core API
|
||||
|
||||
@@ -410,6 +457,12 @@ await ghost.createPost({
|
||||
|
||||
The `SyncedInstance` class enables you to synchronize content across multiple Ghost instances - perfect for staging environments, multi-region deployments, or content distribution.
|
||||
|
||||
**Key Features:**
|
||||
- 🔒 **Same-Instance Protection** - Automatically prevents circular syncs that would cause excessive API calls
|
||||
- 🏷️ **Slug Congruence** - Ensures slugs remain consistent across all synced instances
|
||||
- 🗺️ **ID Mapping** - Tracks source-to-target ID mappings for efficient updates
|
||||
- 📊 **Detailed Reporting** - Get comprehensive sync reports with success/failure counts
|
||||
|
||||
### Setup
|
||||
|
||||
```typescript
|
||||
@@ -433,9 +486,12 @@ const targetGhost2 = new Ghost({
|
||||
adminApiKey: 'target2_admin_key'
|
||||
});
|
||||
|
||||
// This will throw an error if you accidentally try to sync an instance to itself
|
||||
const synced = new SyncedInstance(sourceGhost, [targetGhost1, targetGhost2]);
|
||||
```
|
||||
|
||||
**Safety Note:** SyncedInstance validates that the source and target instances are different. Attempting to sync an instance to itself will throw an error immediately, preventing circular syncs and rate limit issues.
|
||||
|
||||
### Sync Content
|
||||
|
||||
```typescript
|
||||
@@ -505,7 +561,7 @@ synced.clearMappings();
|
||||
Here's a comprehensive example showing various operations:
|
||||
|
||||
```typescript
|
||||
import { Ghost } from '@apiclient.xyz/ghost';
|
||||
import { Ghost, SyncedInstance } from '@apiclient.xyz/ghost';
|
||||
|
||||
const ghost = new Ghost({
|
||||
baseUrl: 'https://your-ghost-site.com',
|
||||
@@ -513,33 +569,134 @@ const ghost = new Ghost({
|
||||
adminApiKey: 'your_admin_key'
|
||||
});
|
||||
|
||||
async function main() {
|
||||
async function createBlogPost() {
|
||||
// Upload a feature image
|
||||
const imageUrl = await ghost.uploadImage('./banner.jpg');
|
||||
|
||||
|
||||
// Create a tag for categorization
|
||||
const tag = await ghost.createTag({
|
||||
name: 'Tutorial',
|
||||
slug: 'tutorial',
|
||||
description: 'Step-by-step guides'
|
||||
description: 'Step-by-step guides',
|
||||
visibility: 'public'
|
||||
});
|
||||
|
||||
// Create a comprehensive blog post
|
||||
const post = await ghost.createPost({
|
||||
title: 'Getting Started with Ghost',
|
||||
html: '<h1>Welcome</h1><p>This is an introduction...</p>',
|
||||
title: 'Getting Started with Ghost CMS',
|
||||
slug: 'getting-started-ghost-cms',
|
||||
html: '<h1>Welcome</h1><p>This is an introduction to Ghost CMS...</p>',
|
||||
feature_image: imageUrl,
|
||||
tags: [{ id: tag.getId() }],
|
||||
featured: true
|
||||
featured: true,
|
||||
status: 'published',
|
||||
meta_title: 'Getting Started with Ghost CMS | Tutorial',
|
||||
meta_description: 'Learn how to get started with Ghost CMS in this comprehensive guide',
|
||||
custom_excerpt: 'A beginner-friendly guide to Ghost CMS'
|
||||
});
|
||||
|
||||
console.log(`Created post: ${post.getTitle()}`);
|
||||
|
||||
const related = await ghost.getRelatedPosts(post.getId(), 5);
|
||||
console.log(`Found ${related.length} related posts`);
|
||||
console.log(`✅ Created post: ${post.getTitle()}`);
|
||||
console.log(`📅 Published at: ${post.postData.published_at}`);
|
||||
|
||||
// Find related content
|
||||
const related = await ghost.getRelatedPosts(post.getId(), 5);
|
||||
console.log(`🔗 Found ${related.length} related posts`);
|
||||
|
||||
// Search functionality
|
||||
const searchResults = await ghost.searchPosts('getting started', { limit: 10 });
|
||||
console.log(`Search found ${searchResults.length} posts`);
|
||||
console.log(`🔍 Search found ${searchResults.length} posts`);
|
||||
|
||||
// Get all public tags
|
||||
const publicTags = await ghost.getPublicTags();
|
||||
console.log(`🏷️ Public tags: ${publicTags.length}`);
|
||||
|
||||
return post;
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
async function syncToStaging() {
|
||||
// Sync content to staging environment
|
||||
const production = new Ghost({
|
||||
baseUrl: 'https://production.ghost.com',
|
||||
adminApiKey: process.env.PROD_ADMIN_KEY,
|
||||
contentApiKey: process.env.PROD_CONTENT_KEY
|
||||
});
|
||||
|
||||
const staging = new Ghost({
|
||||
baseUrl: 'https://staging.ghost.com',
|
||||
adminApiKey: process.env.STAGING_ADMIN_KEY,
|
||||
contentApiKey: process.env.STAGING_CONTENT_KEY
|
||||
});
|
||||
|
||||
const synced = new SyncedInstance(production, [staging]);
|
||||
|
||||
// Sync everything
|
||||
const reports = await synced.syncAll({
|
||||
types: ['tags', 'posts', 'pages']
|
||||
});
|
||||
|
||||
reports.forEach(report => {
|
||||
console.log(`✅ Synced ${report.totalItems} ${report.contentType} in ${report.duration}ms`);
|
||||
});
|
||||
}
|
||||
|
||||
// Run the examples
|
||||
createBlogPost().catch(console.error);
|
||||
// syncToStaging().catch(console.error);
|
||||
```
|
||||
|
||||
## ⚡ Performance & Best Practices
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Ghost enforces rate limits on API requests (~100 requests per IP per hour for Admin API). Keep these tips in mind:
|
||||
|
||||
```typescript
|
||||
// ✅ Good: Batch operations
|
||||
await ghost.bulkUpdatePosts(['id1', 'id2', 'id3'], { featured: true });
|
||||
|
||||
// ❌ Bad: Individual requests in a loop
|
||||
for (const id of postIds) {
|
||||
await ghost.getPostById(id).then(p => p.update({ featured: true }));
|
||||
}
|
||||
|
||||
// ✅ Good: Use pagination efficiently
|
||||
const posts = await ghost.getPosts({ limit: 15 });
|
||||
|
||||
// ✅ Good: Filter on the server side
|
||||
const featuredPosts = await ghost.getPosts({ featured: true, limit: 10 });
|
||||
```
|
||||
|
||||
### Multi-Instance Sync Safety
|
||||
|
||||
The library automatically prevents common pitfalls:
|
||||
|
||||
```typescript
|
||||
// ✅ This works - different instances
|
||||
const synced = new SyncedInstance(sourceGhost, [targetGhost]);
|
||||
|
||||
// ❌ This throws an error - prevents circular sync!
|
||||
const synced = new SyncedInstance(ghost, [ghost]); // Error: Cannot sync to same instance
|
||||
```
|
||||
|
||||
### Content API vs Admin API
|
||||
|
||||
- **Content API**: Read-only, public content, no authentication required (with Content API key)
|
||||
- **Admin API**: Full read/write access, requires Admin API key
|
||||
- **Tags**: This library uses Admin API for tags to fetch ALL tags (Content API only returns tags with posts)
|
||||
|
||||
### Dry Run Mode
|
||||
|
||||
Test your sync operations without making changes:
|
||||
|
||||
```typescript
|
||||
const report = await synced.syncAll({
|
||||
types: ['posts', 'pages', 'tags'],
|
||||
syncOptions: {
|
||||
dryRun: true // Preview changes without applying them
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Would sync ${report[0].totalItems} items`);
|
||||
```
|
||||
|
||||
## 🔒 Error Handling
|
||||
@@ -687,12 +844,31 @@ pnpm test
|
||||
This library is written in TypeScript and provides full type definitions out of the box. No `@types/*` package needed.
|
||||
|
||||
```typescript
|
||||
import type { IPost, ITag, IAuthor, IMember } from '@apiclient.xyz/ghost';
|
||||
import type { IPost, ITag, IAuthor, IMember, IPage } from '@apiclient.xyz/ghost';
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
### Date Handling
|
||||
|
||||
This is an open-source project. Issues and pull requests are welcome!
|
||||
All date fields (`created_at`, `updated_at`, `published_at`) are returned as ISO 8601 formatted strings with timezone information:
|
||||
|
||||
```typescript
|
||||
const post = await ghost.getPostById('post-id');
|
||||
|
||||
// Date strings are in ISO 8601 format: "2025-10-10T13:54:44.000-04:00"
|
||||
console.log(post.postData.created_at); // string
|
||||
console.log(post.postData.updated_at); // string
|
||||
console.log(post.postData.published_at); // string
|
||||
|
||||
// Parse them to Date objects if needed
|
||||
const publishedDate = new Date(post.postData.published_at);
|
||||
console.log(publishedDate.toISOString());
|
||||
```
|
||||
|
||||
**Note:** Ghost automatically manages `updated_at` timestamps. When you update metadata fields (title, status, tags, etc.), Ghost updates this timestamp. HTML-only updates may not always change `updated_at`.
|
||||
|
||||
## 🐛 Issues & Feedback
|
||||
|
||||
Found a bug or have a feature request?
|
||||
|
||||
Repository: [https://code.foss.global/apiclient.xyz/ghost](https://code.foss.global/apiclient.xyz/ghost)
|
||||
|
||||
|
Reference in New Issue
Block a user