@jgardner04/ghost-mcp-server 1.13.4 → 1.13.5
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 +68 -0
- package/package.json +7 -3
- package/src/__tests__/helpers/testUtils.js +15 -1
- package/src/__tests__/mcp_server.test.js +69 -1
- package/src/__tests__/mcp_server_pages.test.js +23 -6
- package/src/mcp_server.js +393 -1143
- package/src/services/__tests__/createResourceService.test.js +468 -0
- package/src/services/__tests__/ghostServiceImproved.members.test.js +0 -3
- package/src/services/__tests__/ghostServiceImproved.newsletters.test.js +2 -2
- package/src/services/__tests__/ghostServiceImproved.pages.test.js +0 -3
- package/src/services/__tests__/ghostServiceImproved.posts.test.js +0 -1
- package/src/services/__tests__/ghostServiceImproved.tags.test.js +0 -3
- package/src/services/__tests__/ghostServiceImproved.tiers.test.js +3 -5
- package/src/services/createResourceService.js +138 -0
- package/src/services/ghostApiClient.js +240 -0
- package/src/services/ghostServiceImproved.js +76 -915
- package/src/services/images.js +27 -0
- package/src/services/members.js +127 -0
- package/src/services/newsletters.js +63 -0
- package/src/services/pages.js +116 -0
- package/src/services/posts.js +116 -0
- package/src/services/tags.js +118 -0
- package/src/services/tiers.js +72 -0
- package/src/services/validators.js +218 -0
|
@@ -1,928 +1,89 @@
|
|
|
1
|
-
import GhostAdminAPI from '@tryghost/admin-api';
|
|
2
|
-
import dotenv from 'dotenv';
|
|
3
|
-
import { promises as fs } from 'fs';
|
|
4
|
-
import {
|
|
5
|
-
GhostAPIError,
|
|
6
|
-
ConfigurationError,
|
|
7
|
-
ValidationError,
|
|
8
|
-
NotFoundError,
|
|
9
|
-
ErrorHandler,
|
|
10
|
-
CircuitBreaker,
|
|
11
|
-
retryWithBackoff,
|
|
12
|
-
} from '../errors/index.js';
|
|
13
|
-
import { createContextLogger } from '../utils/logger.js';
|
|
14
|
-
import { sanitizeNqlValue } from '../utils/nqlSanitizer.js';
|
|
15
|
-
|
|
16
|
-
dotenv.config();
|
|
17
|
-
|
|
18
|
-
const logger = createContextLogger('ghost-service-improved');
|
|
19
|
-
|
|
20
|
-
const { GHOST_ADMIN_API_URL, GHOST_ADMIN_API_KEY } = process.env;
|
|
21
|
-
|
|
22
|
-
// Validate configuration at startup
|
|
23
|
-
if (!GHOST_ADMIN_API_URL || !GHOST_ADMIN_API_KEY) {
|
|
24
|
-
throw new ConfigurationError(
|
|
25
|
-
'Ghost Admin API configuration is incomplete',
|
|
26
|
-
['GHOST_ADMIN_API_URL', 'GHOST_ADMIN_API_KEY'].filter((key) => !process.env[key])
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Configure the Ghost Admin API client
|
|
31
|
-
const api = new GhostAdminAPI({
|
|
32
|
-
url: GHOST_ADMIN_API_URL,
|
|
33
|
-
key: GHOST_ADMIN_API_KEY,
|
|
34
|
-
version: 'v5.0',
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// Circuit breaker for Ghost API
|
|
38
|
-
const ghostCircuitBreaker = new CircuitBreaker({
|
|
39
|
-
failureThreshold: 5,
|
|
40
|
-
resetTimeout: 60000, // 1 minute
|
|
41
|
-
monitoringPeriod: 10000, // 10 seconds
|
|
42
|
-
});
|
|
43
|
-
|
|
44
1
|
/**
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
case 'upload':
|
|
71
|
-
result = await api[resource][action](data);
|
|
72
|
-
break;
|
|
73
|
-
case 'browse':
|
|
74
|
-
case 'read':
|
|
75
|
-
result = await api[resource][action](options, data);
|
|
76
|
-
break;
|
|
77
|
-
case 'delete':
|
|
78
|
-
result = await api[resource][action](data.id || data, options);
|
|
79
|
-
break;
|
|
80
|
-
default:
|
|
81
|
-
result = await api[resource][action](data);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
logger.info('Successfully executed Ghost API request', { operation });
|
|
85
|
-
return result;
|
|
86
|
-
} catch (error) {
|
|
87
|
-
// Transform Ghost API errors into our error types
|
|
88
|
-
throw ErrorHandler.fromGhostError(error, operation);
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// Wrap with circuit breaker if enabled
|
|
93
|
-
const wrappedExecute = useCircuitBreaker
|
|
94
|
-
? () => ghostCircuitBreaker.execute(executeRequest)
|
|
95
|
-
: executeRequest;
|
|
96
|
-
|
|
97
|
-
// Execute with retry logic
|
|
98
|
-
try {
|
|
99
|
-
return await retryWithBackoff(wrappedExecute, {
|
|
100
|
-
maxAttempts: maxRetries,
|
|
101
|
-
onRetry: (attempt, _error) => {
|
|
102
|
-
logger.info('Retrying Ghost API request', { operation, attempt, maxRetries });
|
|
103
|
-
|
|
104
|
-
// Log circuit breaker state if relevant
|
|
105
|
-
if (useCircuitBreaker) {
|
|
106
|
-
const state = ghostCircuitBreaker.getState();
|
|
107
|
-
logger.info('Circuit breaker state', { operation, state });
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
} catch (error) {
|
|
112
|
-
logger.error('Failed to execute Ghost API request', {
|
|
113
|
-
operation,
|
|
114
|
-
maxRetries,
|
|
115
|
-
error: error.message,
|
|
116
|
-
});
|
|
117
|
-
throw error;
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Input validation helpers
|
|
123
|
-
*/
|
|
124
|
-
const validators = {
|
|
125
|
-
validateScheduledStatus(data, resourceLabel = 'Resource') {
|
|
126
|
-
const errors = [];
|
|
127
|
-
|
|
128
|
-
if (data.status === 'scheduled' && !data.published_at) {
|
|
129
|
-
errors.push({
|
|
130
|
-
field: 'published_at',
|
|
131
|
-
message: 'published_at is required when status is scheduled',
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (data.published_at) {
|
|
136
|
-
const publishDate = new Date(data.published_at);
|
|
137
|
-
if (isNaN(publishDate.getTime())) {
|
|
138
|
-
errors.push({ field: 'published_at', message: 'Invalid date format' });
|
|
139
|
-
} else if (data.status === 'scheduled' && publishDate <= new Date()) {
|
|
140
|
-
errors.push({ field: 'published_at', message: 'Scheduled date must be in the future' });
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (errors.length > 0) {
|
|
145
|
-
throw new ValidationError(`${resourceLabel} validation failed`, errors);
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
|
|
149
|
-
validatePostData(postData) {
|
|
150
|
-
const errors = [];
|
|
151
|
-
|
|
152
|
-
if (!postData.title || postData.title.trim().length === 0) {
|
|
153
|
-
errors.push({ field: 'title', message: 'Title is required' });
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (!postData.html && !postData.mobiledoc) {
|
|
157
|
-
errors.push({ field: 'content', message: 'Either html or mobiledoc content is required' });
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (postData.status && !['draft', 'published', 'scheduled'].includes(postData.status)) {
|
|
161
|
-
errors.push({
|
|
162
|
-
field: 'status',
|
|
163
|
-
message: 'Invalid status. Must be draft, published, or scheduled',
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (errors.length > 0) {
|
|
168
|
-
throw new ValidationError('Post validation failed', errors);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
this.validateScheduledStatus(postData, 'Post');
|
|
172
|
-
},
|
|
173
|
-
|
|
174
|
-
validateTagData(tagData) {
|
|
175
|
-
const errors = [];
|
|
176
|
-
|
|
177
|
-
if (!tagData.name || tagData.name.trim().length === 0) {
|
|
178
|
-
errors.push({ field: 'name', message: 'Tag name is required' });
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (tagData.slug && !/^[a-z0-9-]+$/.test(tagData.slug)) {
|
|
182
|
-
errors.push({
|
|
183
|
-
field: 'slug',
|
|
184
|
-
message: 'Slug must contain only lowercase letters, numbers, and hyphens',
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (errors.length > 0) {
|
|
189
|
-
throw new ValidationError('Tag validation failed', errors);
|
|
190
|
-
}
|
|
191
|
-
},
|
|
192
|
-
|
|
193
|
-
validateTagUpdateData(updateData) {
|
|
194
|
-
const errors = [];
|
|
195
|
-
|
|
196
|
-
// Name is optional in updates, but if provided, it cannot be empty
|
|
197
|
-
if (updateData.name !== undefined && updateData.name.trim().length === 0) {
|
|
198
|
-
errors.push({ field: 'name', message: 'Tag name cannot be empty' });
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Validate slug format if provided
|
|
202
|
-
if (updateData.slug && !/^[a-z0-9-]+$/.test(updateData.slug)) {
|
|
203
|
-
errors.push({
|
|
204
|
-
field: 'slug',
|
|
205
|
-
message: 'Slug must contain only lowercase letters, numbers, and hyphens',
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (errors.length > 0) {
|
|
210
|
-
throw new ValidationError('Tag update validation failed', errors);
|
|
211
|
-
}
|
|
212
|
-
},
|
|
213
|
-
|
|
214
|
-
async validateImagePath(imagePath) {
|
|
215
|
-
if (!imagePath || typeof imagePath !== 'string') {
|
|
216
|
-
throw new ValidationError('Image path is required and must be a string');
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Check if file exists
|
|
220
|
-
try {
|
|
221
|
-
await fs.access(imagePath);
|
|
222
|
-
} catch {
|
|
223
|
-
throw new NotFoundError('Image file', imagePath);
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
|
|
227
|
-
validatePageData(pageData) {
|
|
228
|
-
const errors = [];
|
|
229
|
-
|
|
230
|
-
if (!pageData.title || pageData.title.trim().length === 0) {
|
|
231
|
-
errors.push({ field: 'title', message: 'Title is required' });
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (!pageData.html && !pageData.mobiledoc) {
|
|
235
|
-
errors.push({ field: 'content', message: 'Either html or mobiledoc content is required' });
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (pageData.status && !['draft', 'published', 'scheduled'].includes(pageData.status)) {
|
|
239
|
-
errors.push({
|
|
240
|
-
field: 'status',
|
|
241
|
-
message: 'Invalid status. Must be draft, published, or scheduled',
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (errors.length > 0) {
|
|
246
|
-
throw new ValidationError('Page validation failed', errors);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
this.validateScheduledStatus(pageData, 'Page');
|
|
250
|
-
},
|
|
251
|
-
|
|
252
|
-
validateNewsletterData(newsletterData) {
|
|
253
|
-
const errors = [];
|
|
254
|
-
|
|
255
|
-
if (!newsletterData.name || newsletterData.name.trim().length === 0) {
|
|
256
|
-
errors.push({ field: 'name', message: 'Newsletter name is required' });
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (errors.length > 0) {
|
|
260
|
-
throw new ValidationError('Newsletter validation failed', errors);
|
|
261
|
-
}
|
|
262
|
-
},
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Shared CRUD helper: read a single resource by ID with 404→NotFoundError handling
|
|
267
|
-
*/
|
|
268
|
-
async function readResource(resource, id, label, options = {}) {
|
|
269
|
-
if (!id) throw new ValidationError(`${label} ID is required`);
|
|
270
|
-
try {
|
|
271
|
-
return await handleApiRequest(resource, 'read', { id }, options);
|
|
272
|
-
} catch (error) {
|
|
273
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
274
|
-
throw new NotFoundError(label, id);
|
|
275
|
-
}
|
|
276
|
-
throw error;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Shared CRUD helper: update a resource with optimistic concurrency control (OCC)
|
|
282
|
-
* Reads the current version, merges updated_at, then edits.
|
|
283
|
-
*/
|
|
284
|
-
async function updateWithOCC(resource, id, updateData, options = {}, label = resource) {
|
|
285
|
-
const existing = await readResource(resource, id, label);
|
|
286
|
-
const editData = { ...updateData, updated_at: existing.updated_at };
|
|
287
|
-
try {
|
|
288
|
-
return await handleApiRequest(resource, 'edit', { id, ...editData }, options);
|
|
289
|
-
} catch (error) {
|
|
290
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
291
|
-
throw new NotFoundError(label, id);
|
|
292
|
-
}
|
|
293
|
-
throw error;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Shared CRUD helper: delete a resource by ID with 404→NotFoundError handling
|
|
299
|
-
*/
|
|
300
|
-
async function deleteResource(resource, id, label) {
|
|
301
|
-
if (!id) throw new ValidationError(`${label} ID is required for deletion`);
|
|
302
|
-
try {
|
|
303
|
-
return await handleApiRequest(resource, 'delete', { id });
|
|
304
|
-
} catch (error) {
|
|
305
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
306
|
-
throw new NotFoundError(label, id);
|
|
307
|
-
}
|
|
308
|
-
throw error;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Service functions with enhanced error handling
|
|
314
|
-
*/
|
|
315
|
-
|
|
316
|
-
export async function getSiteInfo() {
|
|
317
|
-
return handleApiRequest('site', 'read');
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
export async function createPost(postData, options = { source: 'html' }) {
|
|
321
|
-
// Validate input
|
|
322
|
-
validators.validatePostData(postData);
|
|
323
|
-
|
|
324
|
-
// Add defaults
|
|
325
|
-
const dataWithDefaults = {
|
|
326
|
-
status: 'draft',
|
|
327
|
-
...postData,
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
// SECURITY: HTML must be sanitized before reaching this function. See htmlContentSchema in schemas/common.js
|
|
331
|
-
|
|
332
|
-
try {
|
|
333
|
-
return await handleApiRequest('posts', 'add', dataWithDefaults, options);
|
|
334
|
-
} catch (error) {
|
|
335
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 422) {
|
|
336
|
-
// Transform Ghost validation errors into our format
|
|
337
|
-
throw new ValidationError('Post creation failed due to validation errors', [
|
|
338
|
-
{ field: 'post', message: error.originalError },
|
|
339
|
-
]);
|
|
340
|
-
}
|
|
341
|
-
throw error;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
export async function updatePost(postId, updateData, options = {}) {
|
|
346
|
-
if (!postId) {
|
|
347
|
-
throw new ValidationError('Post ID is required for update');
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Validate scheduled status when status or published_at is being updated
|
|
351
|
-
if (updateData.status || updateData.published_at) {
|
|
352
|
-
let validationData = updateData;
|
|
353
|
-
// When only published_at changes, fetch existing status to check if post is scheduled
|
|
354
|
-
if (!updateData.status && updateData.published_at) {
|
|
355
|
-
const existing = await readResource('posts', postId, 'Post');
|
|
356
|
-
validationData = { ...updateData, status: existing.status };
|
|
357
|
-
}
|
|
358
|
-
validators.validateScheduledStatus(validationData, 'Post');
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return updateWithOCC('posts', postId, updateData, options, 'Post');
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
export async function deletePost(postId) {
|
|
365
|
-
return deleteResource('posts', postId, 'Post');
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
export async function getPost(postId, options = {}) {
|
|
369
|
-
return readResource('posts', postId, 'Post', options);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
export async function getPosts(options = {}) {
|
|
373
|
-
const defaultOptions = {
|
|
374
|
-
limit: 15,
|
|
375
|
-
include: 'tags,authors',
|
|
376
|
-
...options,
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
return handleApiRequest('posts', 'browse', {}, defaultOptions);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
export async function searchPosts(query, options = {}) {
|
|
383
|
-
// Validate query
|
|
384
|
-
if (!query || query.trim().length === 0) {
|
|
385
|
-
throw new ValidationError('Search query is required');
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Sanitize query - escape special NQL characters to prevent injection
|
|
389
|
-
const sanitizedQuery = sanitizeNqlValue(query);
|
|
390
|
-
|
|
391
|
-
// Build filter with fuzzy title match using Ghost NQL
|
|
392
|
-
const filterParts = [`title:~'${sanitizedQuery}'`];
|
|
393
|
-
|
|
394
|
-
// Add status filter if provided and not 'all'
|
|
395
|
-
if (options.status && options.status !== 'all') {
|
|
396
|
-
filterParts.push(`status:${options.status}`);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const searchOptions = {
|
|
400
|
-
limit: options.limit || 15,
|
|
401
|
-
include: 'tags,authors',
|
|
402
|
-
filter: filterParts.join('+'),
|
|
403
|
-
};
|
|
404
|
-
|
|
405
|
-
return handleApiRequest('posts', 'browse', {}, searchOptions);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Page CRUD Operations
|
|
410
|
-
* Pages are similar to posts but do NOT support tags
|
|
411
|
-
*/
|
|
412
|
-
|
|
413
|
-
export async function createPage(pageData, options = { source: 'html' }) {
|
|
414
|
-
// Validate input
|
|
415
|
-
validators.validatePageData(pageData);
|
|
416
|
-
|
|
417
|
-
// Add defaults
|
|
418
|
-
const dataWithDefaults = {
|
|
419
|
-
status: 'draft',
|
|
420
|
-
...pageData,
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
// SECURITY: HTML must be sanitized before reaching this function. See htmlContentSchema in schemas/common.js
|
|
424
|
-
|
|
425
|
-
try {
|
|
426
|
-
return await handleApiRequest('pages', 'add', dataWithDefaults, options);
|
|
427
|
-
} catch (error) {
|
|
428
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 422) {
|
|
429
|
-
throw new ValidationError('Page creation failed due to validation errors', [
|
|
430
|
-
{ field: 'page', message: error.originalError },
|
|
431
|
-
]);
|
|
432
|
-
}
|
|
433
|
-
throw error;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
export async function updatePage(pageId, updateData, options = {}) {
|
|
438
|
-
if (!pageId) {
|
|
439
|
-
throw new ValidationError('Page ID is required for update');
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// SECURITY: HTML must be sanitized before reaching this function. See htmlContentSchema in schemas/common.js
|
|
443
|
-
|
|
444
|
-
// Validate scheduled status when status or published_at is being updated
|
|
445
|
-
if (updateData.status || updateData.published_at) {
|
|
446
|
-
let validationData = updateData;
|
|
447
|
-
// When only published_at changes, fetch existing status to check if page is scheduled
|
|
448
|
-
if (!updateData.status && updateData.published_at) {
|
|
449
|
-
const existing = await readResource('pages', pageId, 'Page');
|
|
450
|
-
validationData = { ...updateData, status: existing.status };
|
|
451
|
-
}
|
|
452
|
-
validators.validateScheduledStatus(validationData, 'Page');
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
return updateWithOCC('pages', pageId, updateData, options, 'Page');
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
export async function deletePage(pageId) {
|
|
459
|
-
return deleteResource('pages', pageId, 'Page');
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
export async function getPage(pageId, options = {}) {
|
|
463
|
-
return readResource('pages', pageId, 'Page', options);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
export async function getPages(options = {}) {
|
|
467
|
-
const defaultOptions = {
|
|
468
|
-
limit: 15,
|
|
469
|
-
include: 'authors',
|
|
470
|
-
...options,
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
return handleApiRequest('pages', 'browse', {}, defaultOptions);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
export async function searchPages(query, options = {}) {
|
|
477
|
-
// Validate query
|
|
478
|
-
if (!query || query.trim().length === 0) {
|
|
479
|
-
throw new ValidationError('Search query is required');
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// Sanitize query - escape special NQL characters to prevent injection
|
|
483
|
-
const sanitizedQuery = sanitizeNqlValue(query);
|
|
484
|
-
|
|
485
|
-
// Build filter with fuzzy title match using Ghost NQL
|
|
486
|
-
const filterParts = [`title:~'${sanitizedQuery}'`];
|
|
487
|
-
|
|
488
|
-
// Add status filter if provided and not 'all'
|
|
489
|
-
if (options.status && options.status !== 'all') {
|
|
490
|
-
filterParts.push(`status:${options.status}`);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const searchOptions = {
|
|
494
|
-
limit: options.limit || 15,
|
|
495
|
-
include: 'authors',
|
|
496
|
-
filter: filterParts.join('+'),
|
|
497
|
-
};
|
|
498
|
-
|
|
499
|
-
return handleApiRequest('pages', 'browse', {}, searchOptions);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
export async function uploadImage(imagePath) {
|
|
503
|
-
// Validate input
|
|
504
|
-
await validators.validateImagePath(imagePath);
|
|
505
|
-
|
|
506
|
-
const imageData = { file: imagePath };
|
|
507
|
-
|
|
508
|
-
try {
|
|
509
|
-
return await handleApiRequest('images', 'upload', imageData);
|
|
510
|
-
} catch (error) {
|
|
511
|
-
if (error instanceof GhostAPIError) {
|
|
512
|
-
throw new ValidationError(`Image upload failed: ${error.originalError}`);
|
|
513
|
-
}
|
|
514
|
-
throw error;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
export async function createTag(tagData) {
|
|
519
|
-
// Validate input
|
|
520
|
-
validators.validateTagData(tagData);
|
|
521
|
-
|
|
522
|
-
// Auto-generate slug if not provided
|
|
523
|
-
if (!tagData.slug) {
|
|
524
|
-
tagData.slug = tagData.name
|
|
525
|
-
.toLowerCase()
|
|
526
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
527
|
-
.replace(/^-+|-+$/g, '');
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
try {
|
|
531
|
-
return await handleApiRequest('tags', 'add', tagData);
|
|
532
|
-
} catch (error) {
|
|
533
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 422) {
|
|
534
|
-
// Check if it's a duplicate tag error
|
|
535
|
-
if (error.originalError.includes('already exists')) {
|
|
536
|
-
// Try to fetch the existing tag by name filter
|
|
537
|
-
const existingTags = await getTags({ filter: `name:'${tagData.name}'` });
|
|
538
|
-
if (existingTags.length > 0) {
|
|
539
|
-
return existingTags[0]; // Return existing tag instead of failing
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
throw new ValidationError('Tag creation failed', [
|
|
543
|
-
{ field: 'tag', message: error.originalError },
|
|
544
|
-
]);
|
|
545
|
-
}
|
|
546
|
-
throw error;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
export async function getTags(options = {}) {
|
|
551
|
-
const tags = await handleApiRequest(
|
|
552
|
-
'tags',
|
|
553
|
-
'browse',
|
|
554
|
-
{},
|
|
555
|
-
{
|
|
556
|
-
limit: 15,
|
|
557
|
-
...options,
|
|
558
|
-
}
|
|
559
|
-
);
|
|
560
|
-
return tags || [];
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
export async function getTag(tagId, options = {}) {
|
|
564
|
-
return readResource('tags', tagId, 'Tag', options);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
export async function updateTag(tagId, updateData) {
|
|
568
|
-
if (!tagId) {
|
|
569
|
-
throw new ValidationError('Tag ID is required for update');
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
validators.validateTagUpdateData(updateData);
|
|
573
|
-
|
|
574
|
-
try {
|
|
575
|
-
await readResource('tags', tagId, 'Tag');
|
|
576
|
-
return await handleApiRequest('tags', 'edit', { id: tagId, ...updateData });
|
|
577
|
-
} catch (error) {
|
|
578
|
-
if (error instanceof NotFoundError) {
|
|
579
|
-
throw error;
|
|
580
|
-
}
|
|
581
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 422) {
|
|
582
|
-
throw new ValidationError('Tag update failed', [
|
|
583
|
-
{ field: 'tag', message: error.originalError },
|
|
584
|
-
]);
|
|
585
|
-
}
|
|
586
|
-
throw error;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
export async function deleteTag(tagId) {
|
|
591
|
-
return deleteResource('tags', tagId, 'Tag');
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* Member CRUD Operations
|
|
596
|
-
* Members represent subscribers/users in Ghost CMS
|
|
597
|
-
*/
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* Creates a new member (subscriber) in Ghost CMS
|
|
601
|
-
* @param {Object} memberData - The member data
|
|
602
|
-
* @param {string} memberData.email - Member email (required)
|
|
603
|
-
* @param {string} [memberData.name] - Member name
|
|
604
|
-
* @param {string} [memberData.note] - Notes about the member (HTML will be sanitized)
|
|
605
|
-
* @param {string[]} [memberData.labels] - Array of label names
|
|
606
|
-
* @param {Object[]} [memberData.newsletters] - Array of newsletter objects with id
|
|
607
|
-
* @param {boolean} [memberData.subscribed] - Email subscription status
|
|
608
|
-
* @param {Object} [options] - Additional options for the API request
|
|
609
|
-
* @returns {Promise<Object>} The created member object
|
|
610
|
-
* @throws {ValidationError} If validation fails
|
|
611
|
-
* @throws {GhostAPIError} If the API request fails
|
|
612
|
-
*/
|
|
613
|
-
export async function createMember(memberData, options = {}) {
|
|
614
|
-
// Input validation is performed at the MCP tool layer using Zod schemas
|
|
615
|
-
try {
|
|
616
|
-
return await handleApiRequest('members', 'add', memberData, options);
|
|
617
|
-
} catch (error) {
|
|
618
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 422) {
|
|
619
|
-
throw new ValidationError('Member creation failed due to validation errors', [
|
|
620
|
-
{ field: 'member', message: error.originalError },
|
|
621
|
-
]);
|
|
622
|
-
}
|
|
623
|
-
throw error;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* Updates an existing member in Ghost CMS
|
|
629
|
-
* @param {string} memberId - The member ID to update
|
|
630
|
-
* @param {Object} updateData - The member update data
|
|
631
|
-
* @param {string} [updateData.email] - Member email
|
|
632
|
-
* @param {string} [updateData.name] - Member name
|
|
633
|
-
* @param {string} [updateData.note] - Notes about the member (HTML will be sanitized)
|
|
634
|
-
* @param {string[]} [updateData.labels] - Array of label names
|
|
635
|
-
* @param {Object[]} [updateData.newsletters] - Array of newsletter objects with id
|
|
636
|
-
* @param {boolean} [updateData.subscribed] - Email subscription status
|
|
637
|
-
* @param {Object} [options] - Additional options for the API request
|
|
638
|
-
* @returns {Promise<Object>} The updated member object
|
|
639
|
-
* @throws {ValidationError} If validation fails
|
|
640
|
-
* @throws {NotFoundError} If the member is not found
|
|
641
|
-
* @throws {GhostAPIError} If the API request fails
|
|
642
|
-
*/
|
|
643
|
-
export async function updateMember(memberId, updateData, options = {}) {
|
|
644
|
-
// Input validation is performed at the MCP tool layer using Zod schemas
|
|
645
|
-
if (!memberId) {
|
|
646
|
-
throw new ValidationError('Member ID is required for update');
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
return updateWithOCC('members', memberId, updateData, options, 'Member');
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* Deletes a member from Ghost CMS
|
|
654
|
-
* @param {string} memberId - The member ID to delete
|
|
655
|
-
* @returns {Promise<Object>} Deletion confirmation object
|
|
656
|
-
* @throws {ValidationError} If member ID is not provided
|
|
657
|
-
* @throws {NotFoundError} If the member is not found
|
|
658
|
-
* @throws {GhostAPIError} If the API request fails
|
|
659
|
-
*/
|
|
660
|
-
export async function deleteMember(memberId) {
|
|
661
|
-
return deleteResource('members', memberId, 'Member');
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/**
|
|
665
|
-
* List members from Ghost CMS with optional filtering and pagination
|
|
666
|
-
* @param {Object} [options] - Query options
|
|
667
|
-
* @param {number} [options.limit] - Number of members to return (1-100)
|
|
668
|
-
* @param {number} [options.page] - Page number (1+)
|
|
669
|
-
* @param {string} [options.filter] - NQL filter string (e.g., 'status:paid')
|
|
670
|
-
* @param {string} [options.order] - Order string (e.g., 'created_at desc')
|
|
671
|
-
* @param {string} [options.include] - Include string (e.g., 'labels,newsletters')
|
|
672
|
-
* @returns {Promise<Array>} Array of member objects
|
|
673
|
-
* @throws {ValidationError} If validation fails
|
|
674
|
-
* @throws {GhostAPIError} If the API request fails
|
|
675
|
-
*/
|
|
676
|
-
export async function getMembers(options = {}) {
|
|
677
|
-
// Input validation is performed at the MCP tool layer using Zod schemas
|
|
678
|
-
const defaultOptions = {
|
|
679
|
-
limit: 15,
|
|
680
|
-
...options,
|
|
681
|
-
};
|
|
682
|
-
|
|
683
|
-
const members = await handleApiRequest('members', 'browse', {}, defaultOptions);
|
|
684
|
-
return members || [];
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
/**
|
|
688
|
-
* Get a single member from Ghost CMS by ID or email
|
|
689
|
-
* @param {Object} params - Lookup parameters (id OR email required)
|
|
690
|
-
* @param {string} [params.id] - Member ID
|
|
691
|
-
* @param {string} [params.email] - Member email
|
|
692
|
-
* @returns {Promise<Object>} The member object
|
|
693
|
-
* @throws {ValidationError} If validation fails
|
|
694
|
-
* @throws {NotFoundError} If the member is not found
|
|
695
|
-
* @throws {GhostAPIError} If the API request fails
|
|
696
|
-
*/
|
|
697
|
-
export async function getMember(params) {
|
|
698
|
-
// Input validation is performed at the MCP tool layer using Zod schemas
|
|
699
|
-
const { id, email } = params;
|
|
700
|
-
|
|
701
|
-
try {
|
|
702
|
-
if (id) {
|
|
703
|
-
// Lookup by ID using read endpoint
|
|
704
|
-
return await handleApiRequest('members', 'read', { id }, { id });
|
|
705
|
-
} else {
|
|
706
|
-
// Lookup by email using browse with filter
|
|
707
|
-
const sanitizedEmail = sanitizeNqlValue(email);
|
|
708
|
-
const members = await handleApiRequest(
|
|
709
|
-
'members',
|
|
710
|
-
'browse',
|
|
711
|
-
{},
|
|
712
|
-
{ filter: `email:'${sanitizedEmail}'`, limit: 1 }
|
|
713
|
-
);
|
|
714
|
-
|
|
715
|
-
if (!members || members.length === 0) {
|
|
716
|
-
throw new NotFoundError('Member', email);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
return members[0];
|
|
720
|
-
}
|
|
721
|
-
} catch (error) {
|
|
722
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
723
|
-
throw new NotFoundError('Member', id || email);
|
|
724
|
-
}
|
|
725
|
-
throw error;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
/**
|
|
730
|
-
* Search members by name or email
|
|
731
|
-
* @param {string} query - Search query (searches name and email fields)
|
|
732
|
-
* @param {Object} [options] - Additional options
|
|
733
|
-
* @param {number} [options.limit] - Maximum number of results (default: 15)
|
|
734
|
-
* @returns {Promise<Array>} Array of matching member objects
|
|
735
|
-
* @throws {ValidationError} If validation fails
|
|
736
|
-
* @throws {GhostAPIError} If the API request fails
|
|
737
|
-
*/
|
|
738
|
-
export async function searchMembers(query, options = {}) {
|
|
739
|
-
// Input validation is performed at the MCP tool layer using Zod schemas
|
|
740
|
-
const sanitizedQuery = sanitizeNqlValue(query.trim());
|
|
741
|
-
|
|
742
|
-
const limit = options.limit || 15;
|
|
743
|
-
|
|
744
|
-
// Build NQL filter for name or email containing the query
|
|
745
|
-
// Ghost uses ~ for contains/like matching
|
|
746
|
-
const filter = `name:~'${sanitizedQuery}',email:~'${sanitizedQuery}'`;
|
|
747
|
-
|
|
748
|
-
const members = await handleApiRequest('members', 'browse', {}, { filter, limit });
|
|
749
|
-
return members || [];
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
/**
|
|
753
|
-
* Newsletter CRUD Operations
|
|
754
|
-
*/
|
|
755
|
-
|
|
756
|
-
export async function getNewsletters(options = {}) {
|
|
757
|
-
const defaultOptions = {
|
|
758
|
-
limit: 'all',
|
|
759
|
-
...options,
|
|
760
|
-
};
|
|
761
|
-
|
|
762
|
-
const newsletters = await handleApiRequest('newsletters', 'browse', {}, defaultOptions);
|
|
763
|
-
return newsletters || [];
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
export async function getNewsletter(newsletterId) {
|
|
767
|
-
return readResource('newsletters', newsletterId, 'Newsletter');
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
export async function createNewsletter(newsletterData) {
|
|
771
|
-
// Validate input
|
|
772
|
-
validators.validateNewsletterData(newsletterData);
|
|
773
|
-
|
|
774
|
-
try {
|
|
775
|
-
return await handleApiRequest('newsletters', 'add', newsletterData);
|
|
776
|
-
} catch (error) {
|
|
777
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 422) {
|
|
778
|
-
throw new ValidationError('Newsletter creation failed', [
|
|
779
|
-
{ field: 'newsletter', message: error.originalError },
|
|
780
|
-
]);
|
|
781
|
-
}
|
|
782
|
-
throw error;
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
export async function updateNewsletter(newsletterId, updateData) {
|
|
787
|
-
if (!newsletterId) {
|
|
788
|
-
throw new ValidationError('Newsletter ID is required for update');
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
try {
|
|
792
|
-
return await updateWithOCC('newsletters', newsletterId, updateData, {}, 'Newsletter');
|
|
793
|
-
} catch (error) {
|
|
794
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 422) {
|
|
795
|
-
throw new ValidationError('Newsletter update failed', [
|
|
796
|
-
{ field: 'newsletter', message: error.originalError },
|
|
797
|
-
]);
|
|
798
|
-
}
|
|
799
|
-
throw error;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
export async function deleteNewsletter(newsletterId) {
|
|
804
|
-
return deleteResource('newsletters', newsletterId, 'Newsletter');
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
/**
|
|
808
|
-
* Create a new tier (membership level)
|
|
809
|
-
* @param {Object} tierData - Tier data
|
|
810
|
-
* @param {Object} [options={}] - Options for the API request
|
|
811
|
-
* @returns {Promise<Object>} Created tier
|
|
812
|
-
*/
|
|
813
|
-
export async function createTier(tierData, options = {}) {
|
|
814
|
-
const { validateTierData } = await import('./tierService.js');
|
|
815
|
-
validateTierData(tierData);
|
|
816
|
-
|
|
817
|
-
try {
|
|
818
|
-
return await handleApiRequest('tiers', 'add', tierData, options);
|
|
819
|
-
} catch (error) {
|
|
820
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 422) {
|
|
821
|
-
throw new ValidationError('Tier creation failed due to validation errors', [
|
|
822
|
-
{ field: 'tier', message: error.originalError },
|
|
823
|
-
]);
|
|
824
|
-
}
|
|
825
|
-
throw error;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
/**
|
|
830
|
-
* Update an existing tier
|
|
831
|
-
* @param {string} id - Tier ID
|
|
832
|
-
* @param {Object} updateData - Tier update data
|
|
833
|
-
* @param {Object} [options={}] - Options for the API request
|
|
834
|
-
* @returns {Promise<Object>} Updated tier
|
|
835
|
-
*/
|
|
836
|
-
export async function updateTier(id, updateData, options = {}) {
|
|
837
|
-
if (!id || typeof id !== 'string' || id.trim().length === 0) {
|
|
838
|
-
throw new ValidationError('Tier ID is required for update');
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
const { validateTierUpdateData } = await import('./tierService.js');
|
|
842
|
-
validateTierUpdateData(updateData);
|
|
843
|
-
|
|
844
|
-
return updateWithOCC('tiers', id, updateData, options, 'Tier');
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
/**
|
|
848
|
-
* Delete a tier
|
|
849
|
-
* @param {string} id - Tier ID
|
|
850
|
-
* @returns {Promise<Object>} Deletion result
|
|
851
|
-
*/
|
|
852
|
-
export async function deleteTier(id) {
|
|
853
|
-
if (!id || typeof id !== 'string' || id.trim().length === 0) {
|
|
854
|
-
throw new ValidationError('Tier ID is required for deletion');
|
|
855
|
-
}
|
|
2
|
+
* Barrel re-export for backward compatibility.
|
|
3
|
+
*
|
|
4
|
+
* The implementation has been split into focused modules:
|
|
5
|
+
* - ghostApiClient.js — API client, circuit breaker, retry logic, CRUD helpers
|
|
6
|
+
* - validators.js — Input validation helpers
|
|
7
|
+
* - posts.js — Post CRUD operations
|
|
8
|
+
* - pages.js — Page CRUD operations
|
|
9
|
+
* - tags.js — Tag CRUD operations
|
|
10
|
+
* - members.js — Member CRUD operations
|
|
11
|
+
* - newsletters.js — Newsletter CRUD operations
|
|
12
|
+
* - tiers.js — Tier CRUD operations
|
|
13
|
+
* - images.js — Image upload
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Infrastructure
|
|
17
|
+
export {
|
|
18
|
+
api,
|
|
19
|
+
ghostCircuitBreaker,
|
|
20
|
+
handleApiRequest,
|
|
21
|
+
readResource,
|
|
22
|
+
updateWithOCC,
|
|
23
|
+
deleteResource,
|
|
24
|
+
getSiteInfo,
|
|
25
|
+
checkHealth,
|
|
26
|
+
} from './ghostApiClient.js';
|
|
856
27
|
|
|
857
|
-
|
|
858
|
-
}
|
|
28
|
+
// Validators
|
|
29
|
+
export { validators } from './validators.js';
|
|
859
30
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
* @param {Object} [options={}] - Query options
|
|
863
|
-
* @param {number} [options.limit] - Number of tiers to return (1-100, default 15)
|
|
864
|
-
* @param {number} [options.page] - Page number
|
|
865
|
-
* @param {string} [options.filter] - NQL filter string (e.g., "type:paid", "type:free")
|
|
866
|
-
* @param {string} [options.order] - Order string
|
|
867
|
-
* @param {string} [options.include] - Include string
|
|
868
|
-
* @returns {Promise<Array>} Array of tiers
|
|
869
|
-
*/
|
|
870
|
-
export async function getTiers(options = {}) {
|
|
871
|
-
const { validateTierQueryOptions } = await import('./tierService.js');
|
|
872
|
-
validateTierQueryOptions(options);
|
|
31
|
+
// Posts
|
|
32
|
+
export { createPost, updatePost, deletePost, getPost, getPosts, searchPosts } from './posts.js';
|
|
873
33
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
...options,
|
|
877
|
-
};
|
|
34
|
+
// Pages
|
|
35
|
+
export { createPage, updatePage, deletePage, getPage, getPages, searchPages } from './pages.js';
|
|
878
36
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
37
|
+
// Tags
|
|
38
|
+
export { createTag, getTags, getTag, updateTag, deleteTag } from './tags.js';
|
|
882
39
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
40
|
+
// Members
|
|
41
|
+
export {
|
|
42
|
+
createMember,
|
|
43
|
+
updateMember,
|
|
44
|
+
deleteMember,
|
|
45
|
+
getMembers,
|
|
46
|
+
getMember,
|
|
47
|
+
searchMembers,
|
|
48
|
+
} from './members.js';
|
|
892
49
|
|
|
893
|
-
|
|
894
|
-
|
|
50
|
+
// Newsletters
|
|
51
|
+
export {
|
|
52
|
+
getNewsletters,
|
|
53
|
+
getNewsletter,
|
|
54
|
+
createNewsletter,
|
|
55
|
+
updateNewsletter,
|
|
56
|
+
deleteNewsletter,
|
|
57
|
+
} from './newsletters.js';
|
|
895
58
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
*/
|
|
899
|
-
export async function checkHealth() {
|
|
900
|
-
try {
|
|
901
|
-
const site = await getSiteInfo();
|
|
902
|
-
const circuitState = ghostCircuitBreaker.getState();
|
|
59
|
+
// Tiers
|
|
60
|
+
export { createTier, updateTier, deleteTier, getTiers, getTier } from './tiers.js';
|
|
903
61
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
site: {
|
|
907
|
-
title: site.title,
|
|
908
|
-
version: site.version,
|
|
909
|
-
url: site.url,
|
|
910
|
-
},
|
|
911
|
-
circuitBreaker: circuitState,
|
|
912
|
-
timestamp: new Date().toISOString(),
|
|
913
|
-
};
|
|
914
|
-
} catch (error) {
|
|
915
|
-
return {
|
|
916
|
-
status: 'unhealthy',
|
|
917
|
-
error: error.message,
|
|
918
|
-
circuitBreaker: ghostCircuitBreaker.getState(),
|
|
919
|
-
timestamp: new Date().toISOString(),
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
}
|
|
62
|
+
// Images
|
|
63
|
+
export { uploadImage } from './images.js';
|
|
923
64
|
|
|
924
|
-
//
|
|
925
|
-
|
|
65
|
+
// Re-import for default export object
|
|
66
|
+
import { getSiteInfo, checkHealth } from './ghostApiClient.js';
|
|
67
|
+
import { createPost, updatePost, deletePost, getPost, getPosts, searchPosts } from './posts.js';
|
|
68
|
+
import { createPage, updatePage, deletePage, getPage, getPages, searchPages } from './pages.js';
|
|
69
|
+
import { createTag, getTags, getTag, updateTag, deleteTag } from './tags.js';
|
|
70
|
+
import {
|
|
71
|
+
createMember,
|
|
72
|
+
updateMember,
|
|
73
|
+
deleteMember,
|
|
74
|
+
getMembers,
|
|
75
|
+
getMember,
|
|
76
|
+
searchMembers,
|
|
77
|
+
} from './members.js';
|
|
78
|
+
import {
|
|
79
|
+
getNewsletters,
|
|
80
|
+
getNewsletter,
|
|
81
|
+
createNewsletter,
|
|
82
|
+
updateNewsletter,
|
|
83
|
+
deleteNewsletter,
|
|
84
|
+
} from './newsletters.js';
|
|
85
|
+
import { createTier, updateTier, deleteTier, getTiers, getTier } from './tiers.js';
|
|
86
|
+
import { uploadImage } from './images.js';
|
|
926
87
|
|
|
927
88
|
export default {
|
|
928
89
|
getSiteInfo,
|