@jgardner04/ghost-mcp-server 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -3
- package/package.json +21 -8
- package/src/config/mcp-config.js +31 -22
- package/src/controllers/imageController.js +62 -62
- package/src/controllers/postController.js +8 -8
- package/src/controllers/tagController.js +17 -20
- package/src/errors/index.js +49 -44
- package/src/index.js +56 -50
- package/src/mcp_server.js +151 -178
- package/src/mcp_server_enhanced.js +265 -259
- package/src/mcp_server_improved.js +217 -582
- package/src/middleware/errorMiddleware.js +69 -70
- package/src/resources/ResourceManager.js +143 -134
- package/src/routes/imageRoutes.js +9 -9
- package/src/routes/postRoutes.js +22 -28
- package/src/routes/tagRoutes.js +12 -14
- package/src/services/__tests__/ghostService.test.js +118 -0
- package/src/services/ghostService.js +34 -46
- package/src/services/ghostServiceImproved.js +125 -109
- package/src/services/imageProcessingService.js +15 -15
- package/src/services/postService.js +22 -22
- package/src/utils/logger.js +50 -50
- package/src/utils/urlValidator.js +37 -38
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import express from
|
|
2
|
-
import rateLimit from
|
|
3
|
-
import { upload, handleImageUpload } from
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import rateLimit from 'express-rate-limit';
|
|
3
|
+
import { upload, handleImageUpload } from '../controllers/imageController.js';
|
|
4
4
|
|
|
5
5
|
const router = express.Router();
|
|
6
6
|
|
|
@@ -9,16 +9,16 @@ const router = express.Router();
|
|
|
9
9
|
const imageUploadRateLimiter = rateLimit({
|
|
10
10
|
windowMs: 60 * 1000, // 1 minute
|
|
11
11
|
max: 10, // Limit each IP to 10 requests per windowMs
|
|
12
|
-
message:
|
|
12
|
+
message: 'Too many image upload attempts from this IP, please try again after a minute',
|
|
13
13
|
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
|
14
14
|
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
|
15
15
|
handler: (req, res) => {
|
|
16
16
|
res.status(429).json({
|
|
17
|
-
error:
|
|
18
|
-
message:
|
|
19
|
-
retryAfter: 60
|
|
17
|
+
error: 'Too many requests',
|
|
18
|
+
message: 'You have exceeded the 10 image uploads per minute limit. Please try again later.',
|
|
19
|
+
retryAfter: 60,
|
|
20
20
|
});
|
|
21
|
-
}
|
|
21
|
+
},
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
// Define the route for uploading an image
|
|
@@ -26,7 +26,7 @@ const imageUploadRateLimiter = rateLimit({
|
|
|
26
26
|
// The `upload.single('image')` middleware handles the file upload.
|
|
27
27
|
// 'image' should match the field name in the form-data request.
|
|
28
28
|
// Added rate limiting to prevent abuse of file system operations
|
|
29
|
-
router.post(
|
|
29
|
+
router.post('/', imageUploadRateLimiter, upload.single('image'), handleImageUpload);
|
|
30
30
|
|
|
31
31
|
// Add other image-related routes here later if needed
|
|
32
32
|
|
package/src/routes/postRoutes.js
CHANGED
|
@@ -1,51 +1,45 @@
|
|
|
1
|
-
import express from
|
|
2
|
-
import { body, validationResult } from
|
|
3
|
-
import { createPost } from
|
|
4
|
-
import { createContextLogger } from
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { body, validationResult } from 'express-validator';
|
|
3
|
+
import { createPost } from '../controllers/postController.js';
|
|
4
|
+
import { createContextLogger } from '../utils/logger.js';
|
|
5
5
|
|
|
6
6
|
const router = express.Router();
|
|
7
7
|
|
|
8
8
|
// Validation middleware for post creation
|
|
9
9
|
const validatePostCreation = [
|
|
10
10
|
// Title must exist and be a non-empty string
|
|
11
|
-
body(
|
|
11
|
+
body('title').notEmpty().withMessage('Post title is required.').isString(),
|
|
12
12
|
// HTML content must exist and be a non-empty string
|
|
13
|
-
body(
|
|
14
|
-
.notEmpty()
|
|
15
|
-
.withMessage("Post HTML content is required.")
|
|
16
|
-
.isString(),
|
|
13
|
+
body('html').notEmpty().withMessage('Post HTML content is required.').isString(),
|
|
17
14
|
// Status must be one of the allowed values if provided
|
|
18
|
-
body(
|
|
15
|
+
body('status')
|
|
19
16
|
.optional()
|
|
20
|
-
.isIn([
|
|
21
|
-
.withMessage(
|
|
17
|
+
.isIn(['published', 'draft', 'scheduled'])
|
|
18
|
+
.withMessage('Invalid status value.'),
|
|
22
19
|
// custom_excerpt should be a string if provided
|
|
23
|
-
body(
|
|
20
|
+
body('custom_excerpt').optional().isString(),
|
|
24
21
|
// published_at should be a valid ISO 8601 date if provided
|
|
25
|
-
body(
|
|
22
|
+
body('published_at')
|
|
26
23
|
.optional()
|
|
27
24
|
.isISO8601()
|
|
28
|
-
.withMessage(
|
|
25
|
+
.withMessage('Invalid date format for published_at (should be ISO 8601).'),
|
|
29
26
|
// tags should be an array if provided
|
|
30
|
-
body(
|
|
27
|
+
body('tags').optional().isArray().withMessage('Tags must be an array.'),
|
|
31
28
|
// Add validation for featured image fields (optional)
|
|
32
|
-
body(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.withMessage("Feature image must be a valid URL."),
|
|
36
|
-
body("feature_image_alt").optional().isString(),
|
|
37
|
-
body("feature_image_caption").optional().isString(),
|
|
29
|
+
body('feature_image').optional().isURL().withMessage('Feature image must be a valid URL.'),
|
|
30
|
+
body('feature_image_alt').optional().isString(),
|
|
31
|
+
body('feature_image_caption').optional().isString(),
|
|
38
32
|
// Add validation for metadata fields (optional strings)
|
|
39
|
-
body(
|
|
33
|
+
body('meta_title')
|
|
40
34
|
.optional()
|
|
41
35
|
.isString()
|
|
42
36
|
.isLength({ max: 300 })
|
|
43
|
-
.withMessage(
|
|
44
|
-
body(
|
|
37
|
+
.withMessage('Meta title cannot exceed 300 characters.'),
|
|
38
|
+
body('meta_description')
|
|
45
39
|
.optional()
|
|
46
40
|
.isString()
|
|
47
41
|
.isLength({ max: 500 })
|
|
48
|
-
.withMessage(
|
|
42
|
+
.withMessage('Meta description cannot exceed 500 characters.'),
|
|
49
43
|
// Handle validation results
|
|
50
44
|
(req, res, next) => {
|
|
51
45
|
const logger = createContextLogger('post-routes');
|
|
@@ -54,7 +48,7 @@ const validatePostCreation = [
|
|
|
54
48
|
// Log the validation errors
|
|
55
49
|
logger.warn('Post validation errors', {
|
|
56
50
|
errors: errors.array(),
|
|
57
|
-
title: req.body?.title
|
|
51
|
+
title: req.body?.title,
|
|
58
52
|
});
|
|
59
53
|
return res.status(400).json({ errors: errors.array() });
|
|
60
54
|
}
|
|
@@ -65,7 +59,7 @@ const validatePostCreation = [
|
|
|
65
59
|
// Define the route for creating a post
|
|
66
60
|
// POST /api/posts
|
|
67
61
|
// Apply the validation middleware before the controller
|
|
68
|
-
router.post(
|
|
62
|
+
router.post('/', validatePostCreation, createPost);
|
|
69
63
|
|
|
70
64
|
// Add other post-related routes here later (e.g., GET /posts/:id, PUT /posts/:id)
|
|
71
65
|
|
package/src/routes/tagRoutes.js
CHANGED
|
@@ -1,25 +1,23 @@
|
|
|
1
|
-
import express from
|
|
2
|
-
import { body, validationResult } from
|
|
3
|
-
import { getTags, createTag } from
|
|
4
|
-
import { createContextLogger } from
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { body, validationResult } from 'express-validator'; // Import for validation
|
|
3
|
+
import { getTags, createTag } from '../controllers/tagController.js';
|
|
4
|
+
import { createContextLogger } from '../utils/logger.js';
|
|
5
5
|
|
|
6
6
|
const router = express.Router();
|
|
7
7
|
|
|
8
8
|
// Validation middleware for tag creation
|
|
9
9
|
const validateTagCreation = [
|
|
10
|
-
body(
|
|
11
|
-
body(
|
|
10
|
+
body('name').notEmpty().withMessage('Tag name is required.').isString(),
|
|
11
|
+
body('description')
|
|
12
12
|
.optional()
|
|
13
13
|
.isString()
|
|
14
14
|
.isLength({ max: 500 })
|
|
15
|
-
.withMessage(
|
|
16
|
-
body(
|
|
15
|
+
.withMessage('Tag description cannot exceed 500 characters.'),
|
|
16
|
+
body('slug')
|
|
17
17
|
.optional()
|
|
18
18
|
.isString()
|
|
19
19
|
.matches(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)
|
|
20
|
-
.withMessage(
|
|
21
|
-
"Tag slug can only contain lower-case letters, numbers, and hyphens."
|
|
22
|
-
),
|
|
20
|
+
.withMessage('Tag slug can only contain lower-case letters, numbers, and hyphens.'),
|
|
23
21
|
// Handle validation results
|
|
24
22
|
(req, res, next) => {
|
|
25
23
|
const logger = createContextLogger('tag-routes');
|
|
@@ -27,7 +25,7 @@ const validateTagCreation = [
|
|
|
27
25
|
if (!errors.isEmpty()) {
|
|
28
26
|
logger.warn('Tag validation errors', {
|
|
29
27
|
errors: errors.array(),
|
|
30
|
-
tagName: req.body?.name
|
|
28
|
+
tagName: req.body?.name,
|
|
31
29
|
});
|
|
32
30
|
return res.status(400).json({ errors: errors.array() });
|
|
33
31
|
}
|
|
@@ -37,10 +35,10 @@ const validateTagCreation = [
|
|
|
37
35
|
|
|
38
36
|
// Define routes
|
|
39
37
|
// GET /api/tags (supports ?name=... query for filtering)
|
|
40
|
-
router.get(
|
|
38
|
+
router.get('/', getTags);
|
|
41
39
|
|
|
42
40
|
// POST /api/tags
|
|
43
|
-
router.post(
|
|
41
|
+
router.post('/', validateTagCreation, createTag);
|
|
44
42
|
|
|
45
43
|
// Add routes for other CRUD operations (GET /:id, PUT /:id, DELETE /:id) later if needed
|
|
46
44
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock the Ghost Admin API
|
|
4
|
+
vi.mock('@tryghost/admin-api', () => {
|
|
5
|
+
const GhostAdminAPI = vi.fn(function () {
|
|
6
|
+
return {
|
|
7
|
+
posts: {
|
|
8
|
+
add: vi.fn(),
|
|
9
|
+
},
|
|
10
|
+
tags: {
|
|
11
|
+
add: vi.fn(),
|
|
12
|
+
browse: vi.fn(),
|
|
13
|
+
},
|
|
14
|
+
site: {
|
|
15
|
+
read: vi.fn(),
|
|
16
|
+
},
|
|
17
|
+
images: {
|
|
18
|
+
upload: vi.fn(),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
default: GhostAdminAPI,
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Mock dotenv
|
|
29
|
+
vi.mock('dotenv', () => ({
|
|
30
|
+
default: {
|
|
31
|
+
config: vi.fn(),
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
// Mock logger
|
|
36
|
+
vi.mock('../../utils/logger.js', () => ({
|
|
37
|
+
createContextLogger: vi.fn(() => ({
|
|
38
|
+
apiRequest: vi.fn(),
|
|
39
|
+
apiResponse: vi.fn(),
|
|
40
|
+
apiError: vi.fn(),
|
|
41
|
+
warn: vi.fn(),
|
|
42
|
+
error: vi.fn(),
|
|
43
|
+
})),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// Import after setting up mocks and environment
|
|
47
|
+
import { createPost, createTag, getTags } from '../ghostService.js';
|
|
48
|
+
|
|
49
|
+
describe('ghostService', () => {
|
|
50
|
+
describe('createPost', () => {
|
|
51
|
+
it('should throw error when title is missing', async () => {
|
|
52
|
+
await expect(createPost({})).rejects.toThrow('Post title is required');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should set default status to draft when not provided', async () => {
|
|
56
|
+
const postData = { title: 'Test Post', html: '<p>Content</p>' };
|
|
57
|
+
|
|
58
|
+
// The function should call the API with default status
|
|
59
|
+
try {
|
|
60
|
+
await createPost(postData);
|
|
61
|
+
} catch (_error) {
|
|
62
|
+
// Expected to fail since we're using a mock, but we can verify the behavior
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
expect(postData.title).toBe('Test Post');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('createTag', () => {
|
|
70
|
+
it('should throw error when tag name is missing', async () => {
|
|
71
|
+
await expect(createTag({})).rejects.toThrow('Tag name is required');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should accept valid tag data', async () => {
|
|
75
|
+
const tagData = { name: 'Test Tag', slug: 'test-tag' };
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await createTag(tagData);
|
|
79
|
+
} catch (_error) {
|
|
80
|
+
// Expected to fail with mock, but validates input handling
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
expect(tagData.name).toBe('Test Tag');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('getTags', () => {
|
|
88
|
+
it('should reject tag names with invalid characters', async () => {
|
|
89
|
+
await expect(getTags("'; DROP TABLE tags; --")).rejects.toThrow(
|
|
90
|
+
'Tag name contains invalid characters'
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should accept valid tag names', async () => {
|
|
95
|
+
const validNames = ['Test Tag', 'test-tag', 'test_tag', 'Tag123'];
|
|
96
|
+
|
|
97
|
+
for (const name of validNames) {
|
|
98
|
+
try {
|
|
99
|
+
await getTags(name);
|
|
100
|
+
} catch (_error) {
|
|
101
|
+
// Expected to fail with mock, but should not throw validation error
|
|
102
|
+
expect(_error.message).not.toContain('invalid characters');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should handle tag names without filter when name is not provided', async () => {
|
|
108
|
+
try {
|
|
109
|
+
await getTags();
|
|
110
|
+
} catch (_error) {
|
|
111
|
+
// Expected to fail with mock
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Should not throw validation error
|
|
115
|
+
expect(true).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import GhostAdminAPI from
|
|
2
|
-
import dotenv from
|
|
3
|
-
import { createContextLogger } from
|
|
1
|
+
import GhostAdminAPI from '@tryghost/admin-api';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
import { createContextLogger } from '../utils/logger.js';
|
|
4
4
|
|
|
5
5
|
dotenv.config();
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ const logger = createContextLogger('ghost-service');
|
|
|
8
8
|
const { GHOST_ADMIN_API_URL, GHOST_ADMIN_API_KEY } = process.env;
|
|
9
9
|
|
|
10
10
|
if (!GHOST_ADMIN_API_URL || !GHOST_ADMIN_API_KEY) {
|
|
11
|
-
throw new Error(
|
|
11
|
+
throw new Error('Ghost Admin API URL and Key must be provided in .env file');
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// Configure the Ghost Admin API client
|
|
@@ -17,7 +17,7 @@ const api = new GhostAdminAPI({
|
|
|
17
17
|
key: GHOST_ADMIN_API_KEY,
|
|
18
18
|
// Specify the Ghost Admin API version
|
|
19
19
|
// Check your Ghost installation for the correct version
|
|
20
|
-
version:
|
|
20
|
+
version: 'v5.0', // Adjust if necessary
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
/**
|
|
@@ -30,21 +30,18 @@ const api = new GhostAdminAPI({
|
|
|
30
30
|
* @param {number} retries - The number of retry attempts remaining.
|
|
31
31
|
* @returns {Promise<object>} The result from the Ghost Admin API.
|
|
32
32
|
*/
|
|
33
|
-
const handleApiRequest = async (
|
|
34
|
-
resource
|
|
35
|
-
action,
|
|
36
|
-
data = {},
|
|
37
|
-
options = {},
|
|
38
|
-
retries = 3
|
|
39
|
-
) => {
|
|
40
|
-
if (!api[resource] || typeof api[resource][action] !== "function") {
|
|
33
|
+
const handleApiRequest = async (resource, action, data = {}, options = {}, retries = 3) => {
|
|
34
|
+
if (!api[resource] || typeof api[resource][action] !== 'function') {
|
|
41
35
|
const errorMsg = `Invalid Ghost API resource or action: ${resource}.${action}`;
|
|
42
36
|
console.error(errorMsg);
|
|
43
37
|
throw new Error(errorMsg);
|
|
44
38
|
}
|
|
45
39
|
|
|
46
40
|
try {
|
|
47
|
-
logger.apiRequest(`${resource}.${action}`, '', {
|
|
41
|
+
logger.apiRequest(`${resource}.${action}`, '', {
|
|
42
|
+
retries,
|
|
43
|
+
hasData: !!Object.keys(data).length,
|
|
44
|
+
});
|
|
48
45
|
// Log data payload carefully, avoiding sensitive info if necessary
|
|
49
46
|
// logger.debug('API request payload', { resource, action, dataKeys: Object.keys(data) });
|
|
50
47
|
|
|
@@ -54,9 +51,9 @@ const handleApiRequest = async (
|
|
|
54
51
|
// Actions like 'browse', 'read' might take options first, then data (like an ID)
|
|
55
52
|
// The Ghost Admin API library structure varies slightly, this is a basic attempt
|
|
56
53
|
// We might need more specific handlers if this proves too simple.
|
|
57
|
-
if (action ===
|
|
54
|
+
if (action === 'add' || action === 'edit') {
|
|
58
55
|
result = await api[resource][action](data, options);
|
|
59
|
-
} else if (action ===
|
|
56
|
+
} else if (action === 'upload') {
|
|
60
57
|
// Upload action has a specific signature
|
|
61
58
|
result = await api[resource][action](data); // data here is { ref, file } or similar
|
|
62
59
|
} else {
|
|
@@ -68,9 +65,9 @@ const handleApiRequest = async (
|
|
|
68
65
|
result = await api[resource][action](data);
|
|
69
66
|
}
|
|
70
67
|
|
|
71
|
-
logger.apiResponse(`${resource}.${action}`, '', 200, {
|
|
68
|
+
logger.apiResponse(`${resource}.${action}`, '', 200, {
|
|
72
69
|
resultType: typeof result,
|
|
73
|
-
hasResult: !!result
|
|
70
|
+
hasResult: !!result,
|
|
74
71
|
});
|
|
75
72
|
return result;
|
|
76
73
|
} catch (error) {
|
|
@@ -81,8 +78,7 @@ const handleApiRequest = async (
|
|
|
81
78
|
const statusCode = error.response?.status; // Example: Check for Axios-like error structure
|
|
82
79
|
const isRateLimit = statusCode === 429;
|
|
83
80
|
const isServerError = statusCode >= 500;
|
|
84
|
-
const isNetworkError =
|
|
85
|
-
error.code === "ECONNREFUSED" || error.code === "ETIMEDOUT"; // Example network errors
|
|
81
|
+
const isNetworkError = error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT'; // Example network errors
|
|
86
82
|
|
|
87
83
|
if ((isRateLimit || isServerError || isNetworkError) && retries > 0) {
|
|
88
84
|
const delay = isRateLimit ? 5000 : 1000 * (4 - retries); // Longer delay for rate limit, increasing delay for others
|
|
@@ -91,7 +87,7 @@ const handleApiRequest = async (
|
|
|
91
87
|
action,
|
|
92
88
|
delay,
|
|
93
89
|
retriesLeft: retries - 1,
|
|
94
|
-
reason: isRateLimit ? 'rate_limit' : isServerError ? 'server_error' : 'network_error'
|
|
90
|
+
reason: isRateLimit ? 'rate_limit' : isServerError ? 'server_error' : 'network_error',
|
|
95
91
|
});
|
|
96
92
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
97
93
|
// Recursively call with decremented retries
|
|
@@ -101,7 +97,7 @@ const handleApiRequest = async (
|
|
|
101
97
|
resource,
|
|
102
98
|
action,
|
|
103
99
|
id: data.id || 'N/A',
|
|
104
|
-
statusCode
|
|
100
|
+
statusCode,
|
|
105
101
|
});
|
|
106
102
|
// Decide how to handle 404 - maybe return null or let the error propagate
|
|
107
103
|
throw error; // Or return null;
|
|
@@ -110,7 +106,7 @@ const handleApiRequest = async (
|
|
|
110
106
|
resource,
|
|
111
107
|
action,
|
|
112
108
|
statusCode,
|
|
113
|
-
error: error.message
|
|
109
|
+
error: error.message,
|
|
114
110
|
});
|
|
115
111
|
throw error; // Re-throw for upstream handling
|
|
116
112
|
}
|
|
@@ -119,7 +115,7 @@ const handleApiRequest = async (
|
|
|
119
115
|
|
|
120
116
|
// Example function (will be expanded later)
|
|
121
117
|
const getSiteInfo = async () => {
|
|
122
|
-
return handleApiRequest(
|
|
118
|
+
return handleApiRequest('site', 'read');
|
|
123
119
|
// try {
|
|
124
120
|
// const site = await api.site.read();
|
|
125
121
|
// console.log("Connected to Ghost site:", site.title);
|
|
@@ -137,19 +133,19 @@ const getSiteInfo = async () => {
|
|
|
137
133
|
* @param {object} options - Optional parameters like source: 'html'.
|
|
138
134
|
* @returns {Promise<object>} The created post object.
|
|
139
135
|
*/
|
|
140
|
-
const createPost = async (postData, options = { source:
|
|
136
|
+
const createPost = async (postData, options = { source: 'html' }) => {
|
|
141
137
|
if (!postData.title) {
|
|
142
|
-
throw new Error(
|
|
138
|
+
throw new Error('Post title is required.');
|
|
143
139
|
}
|
|
144
140
|
// Add more validation as needed (e.g., for content)
|
|
145
141
|
|
|
146
142
|
// Default status to draft if not provided
|
|
147
143
|
const dataWithDefaults = {
|
|
148
|
-
status:
|
|
144
|
+
status: 'draft',
|
|
149
145
|
...postData,
|
|
150
146
|
};
|
|
151
147
|
|
|
152
|
-
return handleApiRequest(
|
|
148
|
+
return handleApiRequest('posts', 'add', dataWithDefaults, options);
|
|
153
149
|
};
|
|
154
150
|
|
|
155
151
|
/**
|
|
@@ -160,14 +156,14 @@ const createPost = async (postData, options = { source: "html" }) => {
|
|
|
160
156
|
*/
|
|
161
157
|
const uploadImage = async (imagePath) => {
|
|
162
158
|
if (!imagePath) {
|
|
163
|
-
throw new Error(
|
|
159
|
+
throw new Error('Image path is required for upload.');
|
|
164
160
|
}
|
|
165
161
|
|
|
166
162
|
// The Ghost Admin API expects an object with a 'file' property containing the path
|
|
167
163
|
const imageData = { file: imagePath };
|
|
168
164
|
|
|
169
165
|
// Use the handleApiRequest function for consistency
|
|
170
|
-
return handleApiRequest(
|
|
166
|
+
return handleApiRequest('images', 'upload', imageData);
|
|
171
167
|
};
|
|
172
168
|
|
|
173
169
|
/**
|
|
@@ -177,10 +173,10 @@ const uploadImage = async (imagePath) => {
|
|
|
177
173
|
*/
|
|
178
174
|
const createTag = async (tagData) => {
|
|
179
175
|
if (!tagData.name) {
|
|
180
|
-
throw new Error(
|
|
176
|
+
throw new Error('Tag name is required.');
|
|
181
177
|
}
|
|
182
178
|
// Ghost automatically generates slug if not provided, but providing is good practice
|
|
183
|
-
return handleApiRequest(
|
|
179
|
+
return handleApiRequest('tags', 'add', tagData);
|
|
184
180
|
};
|
|
185
181
|
|
|
186
182
|
/**
|
|
@@ -190,32 +186,24 @@ const createTag = async (tagData) => {
|
|
|
190
186
|
*/
|
|
191
187
|
const getTags = async (name) => {
|
|
192
188
|
const options = {
|
|
193
|
-
limit:
|
|
189
|
+
limit: 'all', // Get all tags
|
|
194
190
|
};
|
|
195
|
-
|
|
191
|
+
|
|
196
192
|
// Safely construct filter to prevent injection
|
|
197
193
|
if (name) {
|
|
198
194
|
// Additional validation: only allow alphanumeric, spaces, hyphens, underscores
|
|
199
195
|
if (!/^[a-zA-Z0-9\s\-_]+$/.test(name)) {
|
|
200
|
-
throw new Error(
|
|
196
|
+
throw new Error('Tag name contains invalid characters');
|
|
201
197
|
}
|
|
202
198
|
// Escape single quotes and backslashes to prevent injection
|
|
203
199
|
const safeName = name.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
204
200
|
options.filter = `name:'${safeName}'`;
|
|
205
201
|
}
|
|
206
|
-
|
|
207
|
-
return handleApiRequest(
|
|
202
|
+
|
|
203
|
+
return handleApiRequest('tags', 'browse', {}, options);
|
|
208
204
|
};
|
|
209
205
|
|
|
210
206
|
// Add other content management functions here (createTag, etc.)
|
|
211
207
|
|
|
212
208
|
// Export the API client instance and any service functions
|
|
213
|
-
export {
|
|
214
|
-
api,
|
|
215
|
-
getSiteInfo,
|
|
216
|
-
handleApiRequest,
|
|
217
|
-
createPost,
|
|
218
|
-
uploadImage,
|
|
219
|
-
createTag,
|
|
220
|
-
getTags,
|
|
221
|
-
};
|
|
209
|
+
export { api, getSiteInfo, handleApiRequest, createPost, uploadImage, createTag, getTags };
|