@jgardner04/ghost-mcp-server 1.13.1 → 1.13.3
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/package.json +6 -18
- package/src/__tests__/mcp_server.test.js +204 -117
- package/src/__tests__/mcp_server_pages.test.js +32 -18
- package/src/config/mcp-config.js +1 -1
- package/src/controllers/__tests__/tagController.test.js +12 -8
- package/src/controllers/tagController.js +2 -2
- package/src/errors/__tests__/index.test.js +3 -3
- package/src/errors/index.js +1 -1
- package/src/index.js +1 -1
- package/src/mcp_server.js +37 -33
- package/src/schemas/__tests__/postSchemas.test.js +19 -0
- package/src/schemas/__tests__/tagSchemas.test.js +1 -1
- package/src/schemas/common.js +2 -2
- package/src/schemas/memberSchemas.js +20 -8
- package/src/schemas/newsletterSchemas.js +10 -10
- package/src/schemas/pageSchemas.js +16 -13
- package/src/schemas/postSchemas.js +22 -13
- package/src/schemas/tagSchemas.js +12 -7
- package/src/schemas/tierSchemas.js +17 -8
- package/src/services/__tests__/ghostServiceImproved.members.test.js +15 -6
- package/src/services/__tests__/ghostServiceImproved.newsletters.test.js +21 -12
- package/src/services/__tests__/ghostServiceImproved.pages.test.js +59 -16
- package/src/services/__tests__/ghostServiceImproved.posts.test.js +233 -0
- package/src/services/__tests__/ghostServiceImproved.tags.test.js +13 -2
- package/src/services/__tests__/ghostServiceImproved.tiers.test.js +18 -19
- package/src/services/__tests__/memberService.test.js +0 -28
- package/src/services/__tests__/tierService.test.js +0 -28
- package/src/services/ghostServiceImproved.js +108 -379
- package/src/services/imageProcessingService.js +1 -1
- package/src/services/memberService.js +0 -13
- package/src/services/tierService.js +0 -13
- package/src/utils/__tests__/nqlSanitizer.test.js +38 -0
- package/src/utils/__tests__/urlValidator.test.js +137 -1
- package/src/utils/nqlSanitizer.js +11 -0
- package/src/utils/urlValidator.js +25 -2
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import GhostAdminAPI from '@tryghost/admin-api';
|
|
2
|
-
import sanitizeHtml from 'sanitize-html';
|
|
3
2
|
import dotenv from 'dotenv';
|
|
4
3
|
import { promises as fs } from 'fs';
|
|
5
4
|
import {
|
|
@@ -12,6 +11,7 @@ import {
|
|
|
12
11
|
retryWithBackoff,
|
|
13
12
|
} from '../errors/index.js';
|
|
14
13
|
import { createContextLogger } from '../utils/logger.js';
|
|
14
|
+
import { sanitizeNqlValue } from '../utils/nqlSanitizer.js';
|
|
15
15
|
|
|
16
16
|
dotenv.config();
|
|
17
17
|
|
|
@@ -118,6 +118,30 @@ const handleApiRequest = async (resource, action, data = {}, options = {}, confi
|
|
|
118
118
|
* Input validation helpers
|
|
119
119
|
*/
|
|
120
120
|
const validators = {
|
|
121
|
+
validateScheduledStatus(data, resourceLabel = 'Resource') {
|
|
122
|
+
const errors = [];
|
|
123
|
+
|
|
124
|
+
if (data.status === 'scheduled' && !data.published_at) {
|
|
125
|
+
errors.push({
|
|
126
|
+
field: 'published_at',
|
|
127
|
+
message: 'published_at is required when status is scheduled',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (data.published_at) {
|
|
132
|
+
const publishDate = new Date(data.published_at);
|
|
133
|
+
if (isNaN(publishDate.getTime())) {
|
|
134
|
+
errors.push({ field: 'published_at', message: 'Invalid date format' });
|
|
135
|
+
} else if (data.status === 'scheduled' && publishDate <= new Date()) {
|
|
136
|
+
errors.push({ field: 'published_at', message: 'Scheduled date must be in the future' });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (errors.length > 0) {
|
|
141
|
+
throw new ValidationError(`${resourceLabel} validation failed`, errors);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
|
|
121
145
|
validatePostData(postData) {
|
|
122
146
|
const errors = [];
|
|
123
147
|
|
|
@@ -136,25 +160,11 @@ const validators = {
|
|
|
136
160
|
});
|
|
137
161
|
}
|
|
138
162
|
|
|
139
|
-
if (postData.status === 'scheduled' && !postData.published_at) {
|
|
140
|
-
errors.push({
|
|
141
|
-
field: 'published_at',
|
|
142
|
-
message: 'published_at is required when status is scheduled',
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (postData.published_at) {
|
|
147
|
-
const publishDate = new Date(postData.published_at);
|
|
148
|
-
if (isNaN(publishDate.getTime())) {
|
|
149
|
-
errors.push({ field: 'published_at', message: 'Invalid date format' });
|
|
150
|
-
} else if (postData.status === 'scheduled' && publishDate <= new Date()) {
|
|
151
|
-
errors.push({ field: 'published_at', message: 'Scheduled date must be in the future' });
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
163
|
if (errors.length > 0) {
|
|
156
164
|
throw new ValidationError('Post validation failed', errors);
|
|
157
165
|
}
|
|
166
|
+
|
|
167
|
+
this.validateScheduledStatus(postData, 'Post');
|
|
158
168
|
},
|
|
159
169
|
|
|
160
170
|
validateTagData(tagData) {
|
|
@@ -228,25 +238,11 @@ const validators = {
|
|
|
228
238
|
});
|
|
229
239
|
}
|
|
230
240
|
|
|
231
|
-
if (pageData.status === 'scheduled' && !pageData.published_at) {
|
|
232
|
-
errors.push({
|
|
233
|
-
field: 'published_at',
|
|
234
|
-
message: 'published_at is required when status is scheduled',
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (pageData.published_at) {
|
|
239
|
-
const publishDate = new Date(pageData.published_at);
|
|
240
|
-
if (isNaN(publishDate.getTime())) {
|
|
241
|
-
errors.push({ field: 'published_at', message: 'Invalid date format' });
|
|
242
|
-
} else if (pageData.status === 'scheduled' && publishDate <= new Date()) {
|
|
243
|
-
errors.push({ field: 'published_at', message: 'Scheduled date must be in the future' });
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
241
|
if (errors.length > 0) {
|
|
248
242
|
throw new ValidationError('Page validation failed', errors);
|
|
249
243
|
}
|
|
244
|
+
|
|
245
|
+
this.validateScheduledStatus(pageData, 'Page');
|
|
250
246
|
},
|
|
251
247
|
|
|
252
248
|
validateNewsletterData(newsletterData) {
|
|
@@ -262,6 +258,53 @@ const validators = {
|
|
|
262
258
|
},
|
|
263
259
|
};
|
|
264
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Shared CRUD helper: read a single resource by ID with 404→NotFoundError handling
|
|
263
|
+
*/
|
|
264
|
+
async function readResource(resource, id, label, options = {}) {
|
|
265
|
+
if (!id) throw new ValidationError(`${label} ID is required`);
|
|
266
|
+
try {
|
|
267
|
+
return await handleApiRequest(resource, 'read', { id }, options);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
270
|
+
throw new NotFoundError(label, id);
|
|
271
|
+
}
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Shared CRUD helper: update a resource with optimistic concurrency control (OCC)
|
|
278
|
+
* Reads the current version, merges updated_at, then edits.
|
|
279
|
+
*/
|
|
280
|
+
async function updateWithOCC(resource, id, updateData, options = {}, label = resource) {
|
|
281
|
+
const existing = await readResource(resource, id, label);
|
|
282
|
+
const editData = { ...updateData, updated_at: existing.updated_at };
|
|
283
|
+
try {
|
|
284
|
+
return await handleApiRequest(resource, 'edit', { id, ...editData }, options);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
287
|
+
throw new NotFoundError(label, id);
|
|
288
|
+
}
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Shared CRUD helper: delete a resource by ID with 404→NotFoundError handling
|
|
295
|
+
*/
|
|
296
|
+
async function deleteResource(resource, id, label) {
|
|
297
|
+
if (!id) throw new ValidationError(`${label} ID is required for deletion`);
|
|
298
|
+
try {
|
|
299
|
+
return await handleApiRequest(resource, 'delete', { id });
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
302
|
+
throw new NotFoundError(label, id);
|
|
303
|
+
}
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
265
308
|
/**
|
|
266
309
|
* Service functions with enhanced error handling
|
|
267
310
|
*/
|
|
@@ -285,48 +328,7 @@ export async function createPost(postData, options = { source: 'html' }) {
|
|
|
285
328
|
...postData,
|
|
286
329
|
};
|
|
287
330
|
|
|
288
|
-
//
|
|
289
|
-
if (dataWithDefaults.html) {
|
|
290
|
-
// Use proper HTML sanitization library to prevent XSS
|
|
291
|
-
dataWithDefaults.html = sanitizeHtml(dataWithDefaults.html, {
|
|
292
|
-
allowedTags: [
|
|
293
|
-
'h1',
|
|
294
|
-
'h2',
|
|
295
|
-
'h3',
|
|
296
|
-
'h4',
|
|
297
|
-
'h5',
|
|
298
|
-
'h6',
|
|
299
|
-
'blockquote',
|
|
300
|
-
'p',
|
|
301
|
-
'a',
|
|
302
|
-
'ul',
|
|
303
|
-
'ol',
|
|
304
|
-
'nl',
|
|
305
|
-
'li',
|
|
306
|
-
'b',
|
|
307
|
-
'i',
|
|
308
|
-
'strong',
|
|
309
|
-
'em',
|
|
310
|
-
'strike',
|
|
311
|
-
'code',
|
|
312
|
-
'hr',
|
|
313
|
-
'br',
|
|
314
|
-
'div',
|
|
315
|
-
'span',
|
|
316
|
-
'img',
|
|
317
|
-
'pre',
|
|
318
|
-
],
|
|
319
|
-
allowedAttributes: {
|
|
320
|
-
a: ['href', 'title'],
|
|
321
|
-
img: ['src', 'alt', 'title', 'width', 'height'],
|
|
322
|
-
'*': ['class', 'id'],
|
|
323
|
-
},
|
|
324
|
-
allowedSchemes: ['http', 'https', 'mailto'],
|
|
325
|
-
allowedSchemesByTag: {
|
|
326
|
-
img: ['http', 'https', 'data'],
|
|
327
|
-
},
|
|
328
|
-
});
|
|
329
|
-
}
|
|
331
|
+
// SECURITY: HTML must be sanitized before reaching this function. See htmlContentSchema in schemas/common.js
|
|
330
332
|
|
|
331
333
|
try {
|
|
332
334
|
return await handleApiRequest('posts', 'add', dataWithDefaults, options);
|
|
@@ -346,54 +348,20 @@ export async function updatePost(postId, updateData, options = {}) {
|
|
|
346
348
|
throw new ValidationError('Post ID is required for update');
|
|
347
349
|
}
|
|
348
350
|
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
// Merge with existing data
|
|
354
|
-
const mergedData = {
|
|
355
|
-
...existingPost,
|
|
356
|
-
...updateData,
|
|
357
|
-
updated_at: existingPost.updated_at, // Required for Ghost API
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
return await handleApiRequest('posts', 'edit', mergedData, { id: postId, ...options });
|
|
361
|
-
} catch (error) {
|
|
362
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
363
|
-
throw new NotFoundError('Post', postId);
|
|
364
|
-
}
|
|
365
|
-
throw error;
|
|
351
|
+
// Validate scheduled status if status is being updated
|
|
352
|
+
if (updateData.status) {
|
|
353
|
+
validators.validateScheduledStatus(updateData, 'Post');
|
|
366
354
|
}
|
|
355
|
+
|
|
356
|
+
return updateWithOCC('posts', postId, updateData, options, 'Post');
|
|
367
357
|
}
|
|
368
358
|
|
|
369
359
|
export async function deletePost(postId) {
|
|
370
|
-
|
|
371
|
-
throw new ValidationError('Post ID is required for deletion');
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
try {
|
|
375
|
-
return await handleApiRequest('posts', 'delete', { id: postId });
|
|
376
|
-
} catch (error) {
|
|
377
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
378
|
-
throw new NotFoundError('Post', postId);
|
|
379
|
-
}
|
|
380
|
-
throw error;
|
|
381
|
-
}
|
|
360
|
+
return deleteResource('posts', postId, 'Post');
|
|
382
361
|
}
|
|
383
362
|
|
|
384
363
|
export async function getPost(postId, options = {}) {
|
|
385
|
-
|
|
386
|
-
throw new ValidationError('Post ID is required');
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
try {
|
|
390
|
-
return await handleApiRequest('posts', 'read', { id: postId }, options);
|
|
391
|
-
} catch (error) {
|
|
392
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
393
|
-
throw new NotFoundError('Post', postId);
|
|
394
|
-
}
|
|
395
|
-
throw error;
|
|
396
|
-
}
|
|
364
|
+
return readResource('posts', postId, 'Post', options);
|
|
397
365
|
}
|
|
398
366
|
|
|
399
367
|
export async function getPosts(options = {}) {
|
|
@@ -418,7 +386,7 @@ export async function searchPosts(query, options = {}) {
|
|
|
418
386
|
}
|
|
419
387
|
|
|
420
388
|
// Sanitize query - escape special NQL characters to prevent injection
|
|
421
|
-
const sanitizedQuery = query
|
|
389
|
+
const sanitizedQuery = sanitizeNqlValue(query);
|
|
422
390
|
|
|
423
391
|
// Build filter with fuzzy title match using Ghost NQL
|
|
424
392
|
const filterParts = [`title:~'${sanitizedQuery}'`];
|
|
@@ -457,47 +425,7 @@ export async function createPage(pageData, options = { source: 'html' }) {
|
|
|
457
425
|
...pageData,
|
|
458
426
|
};
|
|
459
427
|
|
|
460
|
-
//
|
|
461
|
-
if (dataWithDefaults.html) {
|
|
462
|
-
dataWithDefaults.html = sanitizeHtml(dataWithDefaults.html, {
|
|
463
|
-
allowedTags: [
|
|
464
|
-
'h1',
|
|
465
|
-
'h2',
|
|
466
|
-
'h3',
|
|
467
|
-
'h4',
|
|
468
|
-
'h5',
|
|
469
|
-
'h6',
|
|
470
|
-
'blockquote',
|
|
471
|
-
'p',
|
|
472
|
-
'a',
|
|
473
|
-
'ul',
|
|
474
|
-
'ol',
|
|
475
|
-
'nl',
|
|
476
|
-
'li',
|
|
477
|
-
'b',
|
|
478
|
-
'i',
|
|
479
|
-
'strong',
|
|
480
|
-
'em',
|
|
481
|
-
'strike',
|
|
482
|
-
'code',
|
|
483
|
-
'hr',
|
|
484
|
-
'br',
|
|
485
|
-
'div',
|
|
486
|
-
'span',
|
|
487
|
-
'img',
|
|
488
|
-
'pre',
|
|
489
|
-
],
|
|
490
|
-
allowedAttributes: {
|
|
491
|
-
a: ['href', 'title'],
|
|
492
|
-
img: ['src', 'alt', 'title', 'width', 'height'],
|
|
493
|
-
'*': ['class', 'id'],
|
|
494
|
-
},
|
|
495
|
-
allowedSchemes: ['http', 'https', 'mailto'],
|
|
496
|
-
allowedSchemesByTag: {
|
|
497
|
-
img: ['http', 'https', 'data'],
|
|
498
|
-
},
|
|
499
|
-
});
|
|
500
|
-
}
|
|
428
|
+
// SECURITY: HTML must be sanitized before reaching this function. See htmlContentSchema in schemas/common.js
|
|
501
429
|
|
|
502
430
|
try {
|
|
503
431
|
return await handleApiRequest('pages', 'add', dataWithDefaults, options);
|
|
@@ -516,96 +444,22 @@ export async function updatePage(pageId, updateData, options = {}) {
|
|
|
516
444
|
throw new ValidationError('Page ID is required for update');
|
|
517
445
|
}
|
|
518
446
|
|
|
519
|
-
//
|
|
520
|
-
if (updateData.html) {
|
|
521
|
-
updateData.html = sanitizeHtml(updateData.html, {
|
|
522
|
-
allowedTags: [
|
|
523
|
-
'h1',
|
|
524
|
-
'h2',
|
|
525
|
-
'h3',
|
|
526
|
-
'h4',
|
|
527
|
-
'h5',
|
|
528
|
-
'h6',
|
|
529
|
-
'blockquote',
|
|
530
|
-
'p',
|
|
531
|
-
'a',
|
|
532
|
-
'ul',
|
|
533
|
-
'ol',
|
|
534
|
-
'nl',
|
|
535
|
-
'li',
|
|
536
|
-
'b',
|
|
537
|
-
'i',
|
|
538
|
-
'strong',
|
|
539
|
-
'em',
|
|
540
|
-
'strike',
|
|
541
|
-
'code',
|
|
542
|
-
'hr',
|
|
543
|
-
'br',
|
|
544
|
-
'div',
|
|
545
|
-
'span',
|
|
546
|
-
'img',
|
|
547
|
-
'pre',
|
|
548
|
-
],
|
|
549
|
-
allowedAttributes: {
|
|
550
|
-
a: ['href', 'title'],
|
|
551
|
-
img: ['src', 'alt', 'title', 'width', 'height'],
|
|
552
|
-
'*': ['class', 'id'],
|
|
553
|
-
},
|
|
554
|
-
allowedSchemes: ['http', 'https', 'mailto'],
|
|
555
|
-
allowedSchemesByTag: {
|
|
556
|
-
img: ['http', 'https', 'data'],
|
|
557
|
-
},
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
try {
|
|
562
|
-
// Get existing page to retrieve updated_at for conflict resolution
|
|
563
|
-
const existingPage = await handleApiRequest('pages', 'read', { id: pageId });
|
|
564
|
-
|
|
565
|
-
// Merge existing data with updates, preserving updated_at
|
|
566
|
-
const mergedData = {
|
|
567
|
-
...existingPage,
|
|
568
|
-
...updateData,
|
|
569
|
-
updated_at: existingPage.updated_at,
|
|
570
|
-
};
|
|
447
|
+
// SECURITY: HTML must be sanitized before reaching this function. See htmlContentSchema in schemas/common.js
|
|
571
448
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
throw new NotFoundError('Page', pageId);
|
|
576
|
-
}
|
|
577
|
-
throw error;
|
|
449
|
+
// Validate scheduled status if status is being updated
|
|
450
|
+
if (updateData.status) {
|
|
451
|
+
validators.validateScheduledStatus(updateData, 'Page');
|
|
578
452
|
}
|
|
453
|
+
|
|
454
|
+
return updateWithOCC('pages', pageId, updateData, options, 'Page');
|
|
579
455
|
}
|
|
580
456
|
|
|
581
457
|
export async function deletePage(pageId) {
|
|
582
|
-
|
|
583
|
-
throw new ValidationError('Page ID is required for delete');
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
try {
|
|
587
|
-
return await handleApiRequest('pages', 'delete', { id: pageId });
|
|
588
|
-
} catch (error) {
|
|
589
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
590
|
-
throw new NotFoundError('Page', pageId);
|
|
591
|
-
}
|
|
592
|
-
throw error;
|
|
593
|
-
}
|
|
458
|
+
return deleteResource('pages', pageId, 'Page');
|
|
594
459
|
}
|
|
595
460
|
|
|
596
461
|
export async function getPage(pageId, options = {}) {
|
|
597
|
-
|
|
598
|
-
throw new ValidationError('Page ID is required');
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
try {
|
|
602
|
-
return await handleApiRequest('pages', 'read', { id: pageId }, options);
|
|
603
|
-
} catch (error) {
|
|
604
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
605
|
-
throw new NotFoundError('Page', pageId);
|
|
606
|
-
}
|
|
607
|
-
throw error;
|
|
608
|
-
}
|
|
462
|
+
return readResource('pages', pageId, 'Page', options);
|
|
609
463
|
}
|
|
610
464
|
|
|
611
465
|
export async function getPages(options = {}) {
|
|
@@ -630,7 +484,7 @@ export async function searchPages(query, options = {}) {
|
|
|
630
484
|
}
|
|
631
485
|
|
|
632
486
|
// Sanitize query - escape special NQL characters to prevent injection
|
|
633
|
-
const sanitizedQuery = query
|
|
487
|
+
const sanitizedQuery = sanitizeNqlValue(query);
|
|
634
488
|
|
|
635
489
|
// Build filter with fuzzy title match using Ghost NQL
|
|
636
490
|
const filterParts = [`title:~'${sanitizedQuery}'`];
|
|
@@ -721,18 +575,7 @@ export async function getTags(options = {}) {
|
|
|
721
575
|
}
|
|
722
576
|
|
|
723
577
|
export async function getTag(tagId, options = {}) {
|
|
724
|
-
|
|
725
|
-
throw new ValidationError('Tag ID is required');
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
try {
|
|
729
|
-
return await handleApiRequest('tags', 'read', { id: tagId }, options);
|
|
730
|
-
} catch (error) {
|
|
731
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
732
|
-
throw new NotFoundError('Tag', tagId);
|
|
733
|
-
}
|
|
734
|
-
throw error;
|
|
735
|
-
}
|
|
578
|
+
return readResource('tags', tagId, 'Tag', options);
|
|
736
579
|
}
|
|
737
580
|
|
|
738
581
|
export async function updateTag(tagId, updateData) {
|
|
@@ -740,16 +583,11 @@ export async function updateTag(tagId, updateData) {
|
|
|
740
583
|
throw new ValidationError('Tag ID is required for update');
|
|
741
584
|
}
|
|
742
585
|
|
|
743
|
-
validators.validateTagUpdateData(updateData);
|
|
586
|
+
validators.validateTagUpdateData(updateData);
|
|
744
587
|
|
|
745
588
|
try {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
...existingTag,
|
|
749
|
-
...updateData,
|
|
750
|
-
};
|
|
751
|
-
|
|
752
|
-
return await handleApiRequest('tags', 'edit', mergedData, { id: tagId });
|
|
589
|
+
await readResource('tags', tagId, 'Tag');
|
|
590
|
+
return await handleApiRequest('tags', 'edit', { id: tagId, ...updateData });
|
|
753
591
|
} catch (error) {
|
|
754
592
|
if (error instanceof NotFoundError) {
|
|
755
593
|
throw error;
|
|
@@ -764,18 +602,7 @@ export async function updateTag(tagId, updateData) {
|
|
|
764
602
|
}
|
|
765
603
|
|
|
766
604
|
export async function deleteTag(tagId) {
|
|
767
|
-
|
|
768
|
-
throw new ValidationError('Tag ID is required for deletion');
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
try {
|
|
772
|
-
return await handleApiRequest('tags', 'delete', { id: tagId });
|
|
773
|
-
} catch (error) {
|
|
774
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
775
|
-
throw new NotFoundError('Tag', tagId);
|
|
776
|
-
}
|
|
777
|
-
throw error;
|
|
778
|
-
}
|
|
605
|
+
return deleteResource('tags', tagId, 'Tag');
|
|
779
606
|
}
|
|
780
607
|
|
|
781
608
|
/**
|
|
@@ -833,24 +660,7 @@ export async function updateMember(memberId, updateData, options = {}) {
|
|
|
833
660
|
throw new ValidationError('Member ID is required for update');
|
|
834
661
|
}
|
|
835
662
|
|
|
836
|
-
|
|
837
|
-
// Get existing member to retrieve updated_at for conflict resolution
|
|
838
|
-
const existingMember = await handleApiRequest('members', 'read', { id: memberId });
|
|
839
|
-
|
|
840
|
-
// Merge existing data with updates, preserving updated_at
|
|
841
|
-
const mergedData = {
|
|
842
|
-
...existingMember,
|
|
843
|
-
...updateData,
|
|
844
|
-
updated_at: existingMember.updated_at,
|
|
845
|
-
};
|
|
846
|
-
|
|
847
|
-
return await handleApiRequest('members', 'edit', mergedData, { id: memberId, ...options });
|
|
848
|
-
} catch (error) {
|
|
849
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
850
|
-
throw new NotFoundError('Member', memberId);
|
|
851
|
-
}
|
|
852
|
-
throw error;
|
|
853
|
-
}
|
|
663
|
+
return updateWithOCC('members', memberId, updateData, options, 'Member');
|
|
854
664
|
}
|
|
855
665
|
|
|
856
666
|
/**
|
|
@@ -862,18 +672,7 @@ export async function updateMember(memberId, updateData, options = {}) {
|
|
|
862
672
|
* @throws {GhostAPIError} If the API request fails
|
|
863
673
|
*/
|
|
864
674
|
export async function deleteMember(memberId) {
|
|
865
|
-
|
|
866
|
-
throw new ValidationError('Member ID is required for deletion');
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
try {
|
|
870
|
-
return await handleApiRequest('members', 'delete', { id: memberId });
|
|
871
|
-
} catch (error) {
|
|
872
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
873
|
-
throw new NotFoundError('Member', memberId);
|
|
874
|
-
}
|
|
875
|
-
throw error;
|
|
876
|
-
}
|
|
675
|
+
return deleteResource('members', memberId, 'Member');
|
|
877
676
|
}
|
|
878
677
|
|
|
879
678
|
/**
|
|
@@ -916,7 +715,6 @@ export async function getMembers(options = {}) {
|
|
|
916
715
|
*/
|
|
917
716
|
export async function getMember(params) {
|
|
918
717
|
// Input validation is performed at the MCP tool layer using Zod schemas
|
|
919
|
-
const { sanitizeNqlValue } = await import('./memberService.js');
|
|
920
718
|
const { id, email } = params;
|
|
921
719
|
|
|
922
720
|
try {
|
|
@@ -958,7 +756,6 @@ export async function getMember(params) {
|
|
|
958
756
|
*/
|
|
959
757
|
export async function searchMembers(query, options = {}) {
|
|
960
758
|
// Input validation is performed at the MCP tool layer using Zod schemas
|
|
961
|
-
const { sanitizeNqlValue } = await import('./memberService.js');
|
|
962
759
|
const sanitizedQuery = sanitizeNqlValue(query.trim());
|
|
963
760
|
|
|
964
761
|
const limit = options.limit || 15;
|
|
@@ -996,18 +793,7 @@ export async function getNewsletters(options = {}) {
|
|
|
996
793
|
}
|
|
997
794
|
|
|
998
795
|
export async function getNewsletter(newsletterId) {
|
|
999
|
-
|
|
1000
|
-
throw new ValidationError('Newsletter ID is required');
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
try {
|
|
1004
|
-
return await handleApiRequest('newsletters', 'read', { id: newsletterId });
|
|
1005
|
-
} catch (error) {
|
|
1006
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
1007
|
-
throw new NotFoundError('Newsletter', newsletterId);
|
|
1008
|
-
}
|
|
1009
|
-
throw error;
|
|
1010
|
-
}
|
|
796
|
+
return readResource('newsletters', newsletterId, 'Newsletter');
|
|
1011
797
|
}
|
|
1012
798
|
|
|
1013
799
|
export async function createNewsletter(newsletterData) {
|
|
@@ -1032,23 +818,8 @@ export async function updateNewsletter(newsletterId, updateData) {
|
|
|
1032
818
|
}
|
|
1033
819
|
|
|
1034
820
|
try {
|
|
1035
|
-
|
|
1036
|
-
const existingNewsletter = await handleApiRequest('newsletters', 'read', {
|
|
1037
|
-
id: newsletterId,
|
|
1038
|
-
});
|
|
1039
|
-
|
|
1040
|
-
// Merge existing data with updates, preserving updated_at
|
|
1041
|
-
const mergedData = {
|
|
1042
|
-
...existingNewsletter,
|
|
1043
|
-
...updateData,
|
|
1044
|
-
updated_at: existingNewsletter.updated_at,
|
|
1045
|
-
};
|
|
1046
|
-
|
|
1047
|
-
return await handleApiRequest('newsletters', 'edit', mergedData, { id: newsletterId });
|
|
821
|
+
return await updateWithOCC('newsletters', newsletterId, updateData, {}, 'Newsletter');
|
|
1048
822
|
} catch (error) {
|
|
1049
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
1050
|
-
throw new NotFoundError('Newsletter', newsletterId);
|
|
1051
|
-
}
|
|
1052
823
|
if (error instanceof GhostAPIError && error.ghostStatusCode === 422) {
|
|
1053
824
|
throw new ValidationError('Newsletter update failed', [
|
|
1054
825
|
{ field: 'newsletter', message: error.originalError },
|
|
@@ -1059,18 +830,7 @@ export async function updateNewsletter(newsletterId, updateData) {
|
|
|
1059
830
|
}
|
|
1060
831
|
|
|
1061
832
|
export async function deleteNewsletter(newsletterId) {
|
|
1062
|
-
|
|
1063
|
-
throw new ValidationError('Newsletter ID is required for deletion');
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
try {
|
|
1067
|
-
return await handleApiRequest('newsletters', 'delete', newsletterId);
|
|
1068
|
-
} catch (error) {
|
|
1069
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
1070
|
-
throw new NotFoundError('Newsletter', newsletterId);
|
|
1071
|
-
}
|
|
1072
|
-
throw error;
|
|
1073
|
-
}
|
|
833
|
+
return deleteResource('newsletters', newsletterId, 'Newsletter');
|
|
1074
834
|
}
|
|
1075
835
|
|
|
1076
836
|
/**
|
|
@@ -1110,24 +870,7 @@ export async function updateTier(id, updateData, options = {}) {
|
|
|
1110
870
|
const { validateTierUpdateData } = await import('./tierService.js');
|
|
1111
871
|
validateTierUpdateData(updateData);
|
|
1112
872
|
|
|
1113
|
-
|
|
1114
|
-
// Get existing tier for merge
|
|
1115
|
-
const existingTier = await handleApiRequest('tiers', 'read', { id }, { id });
|
|
1116
|
-
|
|
1117
|
-
// Merge updates with existing data
|
|
1118
|
-
const mergedData = {
|
|
1119
|
-
...existingTier,
|
|
1120
|
-
...updateData,
|
|
1121
|
-
updated_at: existingTier.updated_at,
|
|
1122
|
-
};
|
|
1123
|
-
|
|
1124
|
-
return await handleApiRequest('tiers', 'edit', mergedData, { id, ...options });
|
|
1125
|
-
} catch (error) {
|
|
1126
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
1127
|
-
throw new NotFoundError('Tier', id);
|
|
1128
|
-
}
|
|
1129
|
-
throw error;
|
|
1130
|
-
}
|
|
873
|
+
return updateWithOCC('tiers', id, updateData, options, 'Tier');
|
|
1131
874
|
}
|
|
1132
875
|
|
|
1133
876
|
/**
|
|
@@ -1140,14 +883,7 @@ export async function deleteTier(id) {
|
|
|
1140
883
|
throw new ValidationError('Tier ID is required for deletion');
|
|
1141
884
|
}
|
|
1142
885
|
|
|
1143
|
-
|
|
1144
|
-
return await handleApiRequest('tiers', 'delete', { id });
|
|
1145
|
-
} catch (error) {
|
|
1146
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
1147
|
-
throw new NotFoundError('Tier', id);
|
|
1148
|
-
}
|
|
1149
|
-
throw error;
|
|
1150
|
-
}
|
|
886
|
+
return deleteResource('tiers', id, 'Tier');
|
|
1151
887
|
}
|
|
1152
888
|
|
|
1153
889
|
/**
|
|
@@ -1188,14 +924,7 @@ export async function getTier(id) {
|
|
|
1188
924
|
throw new ValidationError('Tier ID is required and must be a non-empty string');
|
|
1189
925
|
}
|
|
1190
926
|
|
|
1191
|
-
|
|
1192
|
-
return await handleApiRequest('tiers', 'read', { id }, { id });
|
|
1193
|
-
} catch (error) {
|
|
1194
|
-
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
1195
|
-
throw new NotFoundError('Tier', id);
|
|
1196
|
-
}
|
|
1197
|
-
throw error;
|
|
1198
|
-
}
|
|
927
|
+
return readResource('tiers', id, 'Tier');
|
|
1199
928
|
}
|
|
1200
929
|
|
|
1201
930
|
/**
|
|
@@ -89,7 +89,7 @@ const processImage = async (inputPath, outputDir) => {
|
|
|
89
89
|
});
|
|
90
90
|
// If processing fails, maybe fall back to using the original?
|
|
91
91
|
// Or throw the error to fail the upload.
|
|
92
|
-
throw new Error('Image processing failed: ' + error.message);
|
|
92
|
+
throw new Error('Image processing failed: ' + error.message, { cause: error });
|
|
93
93
|
}
|
|
94
94
|
};
|
|
95
95
|
|