@jgardner04/ghost-mcp-server 1.13.3 → 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.
@@ -0,0 +1,27 @@
1
+ import { GhostAPIError, ValidationError } from '../errors/index.js';
2
+ import { handleApiRequest } from './ghostApiClient.js';
3
+ import { validators } from './validators.js';
4
+
5
+ /**
6
+ * Uploads an image to Ghost CMS from a local file path.
7
+ * @param {string} imagePath - Absolute path to the image file
8
+ * @returns {Promise<Object>} The uploaded image object with URL
9
+ * @throws {ValidationError} If the path is invalid or upload fails
10
+ * @throws {NotFoundError} If the file does not exist
11
+ * @throws {GhostAPIError} If the API request fails
12
+ */
13
+ export async function uploadImage(imagePath) {
14
+ // Validate input
15
+ await validators.validateImagePath(imagePath);
16
+
17
+ const imageData = { file: imagePath };
18
+
19
+ try {
20
+ return await handleApiRequest('images', 'upload', imageData);
21
+ } catch (error) {
22
+ if (error instanceof GhostAPIError) {
23
+ throw new ValidationError(`Image upload failed: ${error.originalError}`);
24
+ }
25
+ throw error;
26
+ }
27
+ }
@@ -0,0 +1,127 @@
1
+ import { GhostAPIError, NotFoundError } from '../errors/index.js';
2
+ import { sanitizeNqlValue } from '../utils/nqlSanitizer.js';
3
+ import { handleApiRequest } from './ghostApiClient.js';
4
+ import { createResourceService } from './createResourceService.js';
5
+
6
+ const service = createResourceService({
7
+ resource: 'members',
8
+ label: 'Member',
9
+ listDefaults: { limit: 15 },
10
+ // Input validation is performed at the MCP tool layer using Zod schemas
11
+ });
12
+
13
+ /**
14
+ * Creates a new member (subscriber) in Ghost CMS
15
+ * @param {Object} memberData - The member data
16
+ * @param {string} memberData.email - Member email (required)
17
+ * @param {string} [memberData.name] - Member name
18
+ * @param {string} [memberData.note] - Notes about the member (HTML will be sanitized)
19
+ * @param {string[]} [memberData.labels] - Array of label names
20
+ * @param {Object[]} [memberData.newsletters] - Array of newsletter objects with id
21
+ * @param {boolean} [memberData.subscribed] - Email subscription status
22
+ * @param {Object} [options] - Additional options for the API request
23
+ * @returns {Promise<Object>} The created member object
24
+ * @throws {ValidationError} If validation fails
25
+ * @throws {GhostAPIError} If the API request fails
26
+ */
27
+ export const createMember = service.create;
28
+
29
+ /**
30
+ * Updates an existing member in Ghost CMS
31
+ * @param {string} memberId - The member ID to update
32
+ * @param {Object} updateData - The member update data
33
+ * @param {string} [updateData.email] - Member email
34
+ * @param {string} [updateData.name] - Member name
35
+ * @param {string} [updateData.note] - Notes about the member (HTML will be sanitized)
36
+ * @param {string[]} [updateData.labels] - Array of label names
37
+ * @param {Object[]} [updateData.newsletters] - Array of newsletter objects with id
38
+ * @param {boolean} [updateData.subscribed] - Email subscription status
39
+ * @param {Object} [options] - Additional options for the API request
40
+ * @returns {Promise<Object>} The updated member object
41
+ * @throws {ValidationError} If validation fails
42
+ * @throws {NotFoundError} If the member is not found
43
+ * @throws {GhostAPIError} If the API request fails
44
+ */
45
+ export const updateMember = service.update;
46
+
47
+ /**
48
+ * Deletes a member from Ghost CMS
49
+ * @param {string} memberId - The member ID to delete
50
+ * @returns {Promise<Object>} Deletion confirmation object
51
+ * @throws {ValidationError} If member ID is not provided
52
+ * @throws {NotFoundError} If the member is not found
53
+ * @throws {GhostAPIError} If the API request fails
54
+ */
55
+ export const deleteMember = service.remove;
56
+
57
+ /**
58
+ * List members from Ghost CMS with optional filtering and pagination
59
+ * @param {Object} [options] - Query options
60
+ * @param {number} [options.limit] - Number of members to return (1-100)
61
+ * @param {number} [options.page] - Page number (1+)
62
+ * @param {string} [options.filter] - NQL filter string (e.g., 'status:paid')
63
+ * @param {string} [options.order] - Order string (e.g., 'created_at desc')
64
+ * @param {string} [options.include] - Include string (e.g., 'labels,newsletters')
65
+ * @returns {Promise<Array>} Array of member objects
66
+ * @throws {ValidationError} If validation fails
67
+ * @throws {GhostAPIError} If the API request fails
68
+ */
69
+ export const getMembers = service.getList;
70
+
71
+ /**
72
+ * Get a single member from Ghost CMS by ID or email
73
+ * @param {Object} params - Lookup parameters (id OR email required)
74
+ * @param {string} [params.id] - Member ID
75
+ * @param {string} [params.email] - Member email
76
+ * @returns {Promise<Object>} The member object
77
+ * @throws {ValidationError} If validation fails
78
+ * @throws {NotFoundError} If the member is not found
79
+ * @throws {GhostAPIError} If the API request fails
80
+ */
81
+ export async function getMember(params) {
82
+ const { id, email } = params;
83
+
84
+ try {
85
+ if (id) {
86
+ return await handleApiRequest('members', 'read', { id }, { id });
87
+ } else {
88
+ const sanitizedEmail = sanitizeNqlValue(email);
89
+ const members = await handleApiRequest(
90
+ 'members',
91
+ 'browse',
92
+ {},
93
+ { filter: `email:'${sanitizedEmail}'`, limit: 1 }
94
+ );
95
+
96
+ if (!members || members.length === 0) {
97
+ throw new NotFoundError('Member', email);
98
+ }
99
+
100
+ return members[0];
101
+ }
102
+ } catch (error) {
103
+ if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
104
+ throw new NotFoundError('Member', id || email);
105
+ }
106
+ throw error;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Search members by name or email
112
+ * @param {string} query - Search query (searches name and email fields)
113
+ * @param {Object} [options] - Additional options
114
+ * @param {number} [options.limit] - Maximum number of results (default: 15)
115
+ * @returns {Promise<Array>} Array of matching member objects
116
+ * @throws {ValidationError} If validation fails
117
+ * @throws {GhostAPIError} If the API request fails
118
+ */
119
+ export async function searchMembers(query, options = {}) {
120
+ const sanitizedQuery = sanitizeNqlValue(query.trim());
121
+ const limit = options.limit || 15;
122
+
123
+ const filter = `name:~'${sanitizedQuery}',email:~'${sanitizedQuery}'`;
124
+
125
+ const members = await handleApiRequest('members', 'browse', {}, { filter, limit });
126
+ return members || [];
127
+ }
@@ -0,0 +1,63 @@
1
+ import { createResourceService } from './createResourceService.js';
2
+ import { validators } from './validators.js';
3
+
4
+ const service = createResourceService({
5
+ resource: 'newsletters',
6
+ label: 'Newsletter',
7
+ listDefaults: { limit: 'all' },
8
+ validateCreate: (data) => validators.validateNewsletterData(data),
9
+ catch422OnUpdate: true,
10
+ });
11
+
12
+ /**
13
+ * Lists all newsletters with optional filtering and pagination.
14
+ * @param {Object} [options={}] - Query options
15
+ * @param {string|number} [options.limit='all'] - Number of newsletters to return (default: 'all')
16
+ * @returns {Promise<Array>} Array of newsletter objects (empty array if none found)
17
+ * @throws {GhostAPIError} If the API request fails
18
+ */
19
+ export const getNewsletters = service.getList;
20
+
21
+ /**
22
+ * Retrieves a single newsletter by ID.
23
+ * @param {string} newsletterId - The newsletter ID to retrieve
24
+ * @returns {Promise<Object>} The newsletter object
25
+ * @throws {ValidationError} If the newsletter ID is missing
26
+ * @throws {NotFoundError} If the newsletter is not found
27
+ * @throws {GhostAPIError} If the API request fails
28
+ */
29
+ export const getNewsletter = service.getOne;
30
+
31
+ /**
32
+ * Creates a new newsletter in Ghost CMS.
33
+ * @param {Object} newsletterData - The newsletter data
34
+ * @param {string} newsletterData.name - Newsletter name (required)
35
+ * @param {string} [newsletterData.description] - Newsletter description
36
+ * @param {string} [newsletterData.sender_name] - Sender name
37
+ * @param {string} [newsletterData.sender_email] - Sender email
38
+ * @returns {Promise<Object>} The created newsletter object
39
+ * @throws {ValidationError} If validation fails or Ghost returns a 422
40
+ * @throws {GhostAPIError} If the API request fails
41
+ */
42
+ export const createNewsletter = service.create;
43
+
44
+ /**
45
+ * Updates an existing newsletter with optimistic concurrency control.
46
+ * @param {string} newsletterId - The newsletter ID to update
47
+ * @param {Object} updateData - Fields to update on the newsletter
48
+ * @returns {Promise<Object>} The updated newsletter object
49
+ * @throws {ValidationError} If the newsletter ID is missing or Ghost returns a 422
50
+ * @throws {NotFoundError} If the newsletter is not found
51
+ * @throws {GhostAPIError} If the API request fails
52
+ */
53
+ export const updateNewsletter = service.update;
54
+
55
+ /**
56
+ * Deletes a newsletter by ID.
57
+ * @param {string} newsletterId - The newsletter ID to delete
58
+ * @returns {Promise<Object>} Deletion confirmation
59
+ * @throws {ValidationError} If the newsletter ID is missing
60
+ * @throws {NotFoundError} If the newsletter is not found
61
+ * @throws {GhostAPIError} If the API request fails
62
+ */
63
+ export const deleteNewsletter = service.remove;
@@ -0,0 +1,116 @@
1
+ import { ValidationError } from '../errors/index.js';
2
+ import { sanitizeNqlValue } from '../utils/nqlSanitizer.js';
3
+ import { handleApiRequest, readResource } from './ghostApiClient.js';
4
+ import { createResourceService } from './createResourceService.js';
5
+ import { validators } from './validators.js';
6
+
7
+ const service = createResourceService({
8
+ resource: 'pages',
9
+ label: 'Page',
10
+ createDefaults: { status: 'draft' },
11
+ createOptions: { source: 'html' },
12
+ listDefaults: { limit: 15, include: 'authors' },
13
+ validateCreate: (data) => validators.validatePageData(data),
14
+ validateUpdate: async (id, updateData) => {
15
+ if (updateData.status || updateData.published_at) {
16
+ let validationData = updateData;
17
+ if (!updateData.status && updateData.published_at) {
18
+ const existing = await readResource('pages', id, 'Page');
19
+ validationData = { ...updateData, status: existing.status };
20
+ }
21
+ validators.validateScheduledStatus(validationData, 'Page');
22
+ }
23
+ },
24
+ });
25
+
26
+ /**
27
+ * Creates a new page in Ghost CMS.
28
+ * @param {Object} pageData - The page data
29
+ * @param {string} pageData.title - Page title (required)
30
+ * @param {string} [pageData.html] - HTML content (required if mobiledoc not provided)
31
+ * @param {string} [pageData.mobiledoc] - Mobiledoc content (required if html not provided)
32
+ * @param {string} [pageData.status='draft'] - Page status ('draft', 'published', 'scheduled')
33
+ * @param {string} [pageData.published_at] - ISO 8601 date for scheduled pages
34
+ * @param {Object} [options={ source: 'html' }] - API request options
35
+ * @returns {Promise<Object>} The created page object
36
+ * @throws {ValidationError} If validation fails or Ghost returns a 422
37
+ * @throws {GhostAPIError} If the API request fails
38
+ */
39
+ export const createPage = service.create;
40
+
41
+ /**
42
+ * Updates an existing page with optimistic concurrency control.
43
+ * Validates scheduling fields when status or published_at is being changed.
44
+ * @param {string} pageId - The page ID to update
45
+ * @param {Object} updateData - Fields to update on the page
46
+ * @param {Object} [options={}] - API request options
47
+ * @returns {Promise<Object>} The updated page object
48
+ * @throws {ValidationError} If the page ID is missing or scheduling validation fails
49
+ * @throws {NotFoundError} If the page is not found
50
+ * @throws {GhostAPIError} If the API request fails
51
+ */
52
+ export const updatePage = service.update;
53
+
54
+ /**
55
+ * Deletes a page by ID.
56
+ * @param {string} pageId - The page ID to delete
57
+ * @returns {Promise<Object>} Deletion confirmation
58
+ * @throws {ValidationError} If the page ID is missing
59
+ * @throws {NotFoundError} If the page is not found
60
+ * @throws {GhostAPIError} If the API request fails
61
+ */
62
+ export const deletePage = service.remove;
63
+
64
+ /**
65
+ * Retrieves a single page by ID.
66
+ * @param {string} pageId - The page ID to retrieve
67
+ * @param {Object} [options={}] - API request options (e.g., includes)
68
+ * @returns {Promise<Object>} The page object
69
+ * @throws {ValidationError} If the page ID is missing
70
+ * @throws {NotFoundError} If the page is not found
71
+ * @throws {GhostAPIError} If the API request fails
72
+ */
73
+ export const getPage = service.getOne;
74
+
75
+ /**
76
+ * Lists pages with optional filtering and pagination.
77
+ * @param {Object} [options={}] - Query options
78
+ * @param {number} [options.limit=15] - Number of pages to return
79
+ * @param {string} [options.include='authors'] - Related resources to include
80
+ * @param {string} [options.filter] - NQL filter string
81
+ * @param {string} [options.order] - Order string
82
+ * @returns {Promise<Array>} Array of page objects
83
+ * @throws {GhostAPIError} If the API request fails
84
+ */
85
+ export const getPages = service.getList;
86
+
87
+ /**
88
+ * Searches pages by title using Ghost NQL fuzzy matching.
89
+ * @param {string} query - Search query string (required, non-empty)
90
+ * @param {Object} [options={}] - Additional search options
91
+ * @param {string} [options.status] - Filter by status ('draft', 'published', 'scheduled', or 'all')
92
+ * @param {number} [options.limit=15] - Maximum number of results
93
+ * @returns {Promise<Array>} Array of matching page objects
94
+ * @throws {ValidationError} If the query is empty
95
+ * @throws {GhostAPIError} If the API request fails
96
+ */
97
+ export async function searchPages(query, options = {}) {
98
+ if (!query || query.trim().length === 0) {
99
+ throw new ValidationError('Search query is required');
100
+ }
101
+
102
+ const sanitizedQuery = sanitizeNqlValue(query);
103
+
104
+ const filterParts = [`title:~'${sanitizedQuery}'`];
105
+ if (options.status && options.status !== 'all') {
106
+ filterParts.push(`status:${options.status}`);
107
+ }
108
+
109
+ const searchOptions = {
110
+ limit: options.limit || 15,
111
+ include: 'authors',
112
+ filter: filterParts.join('+'),
113
+ };
114
+
115
+ return handleApiRequest('pages', 'browse', {}, searchOptions);
116
+ }
@@ -0,0 +1,116 @@
1
+ import { ValidationError } from '../errors/index.js';
2
+ import { sanitizeNqlValue } from '../utils/nqlSanitizer.js';
3
+ import { handleApiRequest, readResource } from './ghostApiClient.js';
4
+ import { createResourceService } from './createResourceService.js';
5
+ import { validators } from './validators.js';
6
+
7
+ const service = createResourceService({
8
+ resource: 'posts',
9
+ label: 'Post',
10
+ createDefaults: { status: 'draft' },
11
+ createOptions: { source: 'html' },
12
+ listDefaults: { limit: 15, include: 'tags,authors' },
13
+ validateCreate: (data) => validators.validatePostData(data),
14
+ validateUpdate: async (id, updateData) => {
15
+ if (updateData.status || updateData.published_at) {
16
+ let validationData = updateData;
17
+ if (!updateData.status && updateData.published_at) {
18
+ const existing = await readResource('posts', id, 'Post');
19
+ validationData = { ...updateData, status: existing.status };
20
+ }
21
+ validators.validateScheduledStatus(validationData, 'Post');
22
+ }
23
+ },
24
+ });
25
+
26
+ /**
27
+ * Creates a new post in Ghost CMS.
28
+ * @param {Object} postData - The post data
29
+ * @param {string} postData.title - Post title (required)
30
+ * @param {string} [postData.html] - HTML content (required if mobiledoc not provided)
31
+ * @param {string} [postData.mobiledoc] - Mobiledoc content (required if html not provided)
32
+ * @param {string} [postData.status='draft'] - Post status ('draft', 'published', 'scheduled')
33
+ * @param {string} [postData.published_at] - ISO 8601 date for scheduled posts
34
+ * @param {Object} [options={ source: 'html' }] - API request options
35
+ * @returns {Promise<Object>} The created post object
36
+ * @throws {ValidationError} If validation fails or Ghost returns a 422
37
+ * @throws {GhostAPIError} If the API request fails
38
+ */
39
+ export const createPost = service.create;
40
+
41
+ /**
42
+ * Updates an existing post with optimistic concurrency control.
43
+ * Validates scheduling fields when status or published_at is being changed.
44
+ * @param {string} postId - The post ID to update
45
+ * @param {Object} updateData - Fields to update on the post
46
+ * @param {Object} [options={}] - API request options
47
+ * @returns {Promise<Object>} The updated post object
48
+ * @throws {ValidationError} If the post ID is missing or scheduling validation fails
49
+ * @throws {NotFoundError} If the post is not found
50
+ * @throws {GhostAPIError} If the API request fails
51
+ */
52
+ export const updatePost = service.update;
53
+
54
+ /**
55
+ * Deletes a post by ID.
56
+ * @param {string} postId - The post ID to delete
57
+ * @returns {Promise<Object>} Deletion confirmation
58
+ * @throws {ValidationError} If the post ID is missing
59
+ * @throws {NotFoundError} If the post is not found
60
+ * @throws {GhostAPIError} If the API request fails
61
+ */
62
+ export const deletePost = service.remove;
63
+
64
+ /**
65
+ * Retrieves a single post by ID.
66
+ * @param {string} postId - The post ID to retrieve
67
+ * @param {Object} [options={}] - API request options (e.g., includes)
68
+ * @returns {Promise<Object>} The post object
69
+ * @throws {ValidationError} If the post ID is missing
70
+ * @throws {NotFoundError} If the post is not found
71
+ * @throws {GhostAPIError} If the API request fails
72
+ */
73
+ export const getPost = service.getOne;
74
+
75
+ /**
76
+ * Lists posts with optional filtering and pagination.
77
+ * @param {Object} [options={}] - Query options
78
+ * @param {number} [options.limit=15] - Number of posts to return
79
+ * @param {string} [options.include='tags,authors'] - Related resources to include
80
+ * @param {string} [options.filter] - NQL filter string
81
+ * @param {string} [options.order] - Order string (e.g., 'published_at desc')
82
+ * @returns {Promise<Array>} Array of post objects
83
+ * @throws {GhostAPIError} If the API request fails
84
+ */
85
+ export const getPosts = service.getList;
86
+
87
+ /**
88
+ * Searches posts by title using Ghost NQL fuzzy matching.
89
+ * @param {string} query - Search query string (required, non-empty)
90
+ * @param {Object} [options={}] - Additional search options
91
+ * @param {string} [options.status] - Filter by status ('draft', 'published', 'scheduled', or 'all')
92
+ * @param {number} [options.limit=15] - Maximum number of results
93
+ * @returns {Promise<Array>} Array of matching post objects
94
+ * @throws {ValidationError} If the query is empty
95
+ * @throws {GhostAPIError} If the API request fails
96
+ */
97
+ export async function searchPosts(query, options = {}) {
98
+ if (!query || query.trim().length === 0) {
99
+ throw new ValidationError('Search query is required');
100
+ }
101
+
102
+ const sanitizedQuery = sanitizeNqlValue(query);
103
+
104
+ const filterParts = [`title:~'${sanitizedQuery}'`];
105
+ if (options.status && options.status !== 'all') {
106
+ filterParts.push(`status:${options.status}`);
107
+ }
108
+
109
+ const searchOptions = {
110
+ limit: options.limit || 15,
111
+ include: 'tags,authors',
112
+ filter: filterParts.join('+'),
113
+ };
114
+
115
+ return handleApiRequest('posts', 'browse', {}, searchOptions);
116
+ }
@@ -0,0 +1,118 @@
1
+ import { GhostAPIError, ValidationError, NotFoundError } from '../errors/index.js';
2
+ import { sanitizeNqlValue } from '../utils/nqlSanitizer.js';
3
+ import { handleApiRequest, readResource } from './ghostApiClient.js';
4
+ import { createResourceService } from './createResourceService.js';
5
+ import { validators } from './validators.js';
6
+
7
+ const service = createResourceService({
8
+ resource: 'tags',
9
+ label: 'Tag',
10
+ listDefaults: { limit: 15 },
11
+ });
12
+
13
+ /**
14
+ * Creates a new tag in Ghost CMS. Auto-generates a slug from the name if not provided.
15
+ * If a tag with the same name already exists, returns the existing tag instead of failing.
16
+ * @param {Object} tagData - The tag data
17
+ * @param {string} tagData.name - Tag name (required)
18
+ * @param {string} [tagData.slug] - Tag slug (auto-generated from name if omitted)
19
+ * @param {string} [tagData.description] - Tag description
20
+ * @returns {Promise<Object>} The created (or existing) tag object
21
+ * @throws {ValidationError} If validation fails or Ghost returns a 422 (non-duplicate)
22
+ * @throws {GhostAPIError} If the API request fails
23
+ */
24
+ export async function createTag(tagData) {
25
+ validators.validateTagData(tagData);
26
+
27
+ const dataToCreate = tagData.slug
28
+ ? { ...tagData }
29
+ : {
30
+ ...tagData,
31
+ slug: tagData.name
32
+ .toLowerCase()
33
+ .replace(/[^a-z0-9]+/g, '-')
34
+ .replace(/^-+|-+$/g, ''),
35
+ };
36
+
37
+ try {
38
+ return await handleApiRequest('tags', 'add', dataToCreate);
39
+ } catch (error) {
40
+ if (error instanceof GhostAPIError && error.ghostStatusCode === 422) {
41
+ if (error.originalError.includes('already exists')) {
42
+ const existingTags = await getTags({ filter: `name:'${sanitizeNqlValue(tagData.name)}'` });
43
+ if (existingTags.length > 0) {
44
+ return existingTags[0];
45
+ }
46
+ }
47
+ throw new ValidationError('Tag creation failed', [
48
+ { field: 'tag', message: error.originalError },
49
+ ]);
50
+ }
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Lists tags with optional filtering and pagination.
57
+ * @param {Object} [options={}] - Query options
58
+ * @param {number} [options.limit=15] - Number of tags to return
59
+ * @param {string} [options.filter] - NQL filter string
60
+ * @param {string} [options.order] - Order string
61
+ * @returns {Promise<Array>} Array of tag objects (empty array if none found)
62
+ * @throws {GhostAPIError} If the API request fails
63
+ */
64
+ export const getTags = service.getList;
65
+
66
+ /**
67
+ * Retrieves a single tag by ID.
68
+ * @param {string} tagId - The tag ID to retrieve
69
+ * @param {Object} [options={}] - API request options
70
+ * @returns {Promise<Object>} The tag object
71
+ * @throws {ValidationError} If the tag ID is missing
72
+ * @throws {NotFoundError} If the tag is not found
73
+ * @throws {GhostAPIError} If the API request fails
74
+ */
75
+ export const getTag = service.getOne;
76
+
77
+ /**
78
+ * Updates an existing tag. Validates update data and checks that the tag exists first.
79
+ * Tags use direct edit (not OCC) since they don't have concurrent editing concerns.
80
+ * @param {string} tagId - The tag ID to update
81
+ * @param {Object} updateData - Fields to update on the tag
82
+ * @param {string} [updateData.name] - Updated tag name
83
+ * @param {string} [updateData.slug] - Updated tag slug
84
+ * @param {string} [updateData.description] - Updated tag description
85
+ * @returns {Promise<Object>} The updated tag object
86
+ * @throws {ValidationError} If the tag ID is missing or update data is invalid
87
+ * @throws {NotFoundError} If the tag is not found
88
+ * @throws {GhostAPIError} If the API request fails
89
+ */
90
+ export async function updateTag(tagId, updateData) {
91
+ validators.requireId(tagId, 'Tag');
92
+ validators.validateTagUpdateData(updateData);
93
+
94
+ try {
95
+ await readResource('tags', tagId, 'Tag');
96
+ return await handleApiRequest('tags', 'edit', { id: tagId, ...updateData });
97
+ } catch (error) {
98
+ if (error instanceof NotFoundError) {
99
+ throw error;
100
+ }
101
+ if (error instanceof GhostAPIError && error.ghostStatusCode === 422) {
102
+ throw new ValidationError('Tag update failed', [
103
+ { field: 'tag', message: error.originalError },
104
+ ]);
105
+ }
106
+ throw error;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Deletes a tag by ID.
112
+ * @param {string} tagId - The tag ID to delete
113
+ * @returns {Promise<Object>} Deletion confirmation
114
+ * @throws {ValidationError} If the tag ID is missing
115
+ * @throws {NotFoundError} If the tag is not found
116
+ * @throws {GhostAPIError} If the API request fails
117
+ */
118
+ export const deleteTag = service.remove;
@@ -0,0 +1,72 @@
1
+ import { createResourceService } from './createResourceService.js';
2
+ import {
3
+ validateTierData,
4
+ validateTierUpdateData,
5
+ validateTierQueryOptions,
6
+ } from './tierService.js';
7
+
8
+ const service = createResourceService({
9
+ resource: 'tiers',
10
+ label: 'Tier',
11
+ listDefaults: { limit: 15 },
12
+ validateCreate: (data) => validateTierData(data),
13
+ validateUpdate: (_id, data) => validateTierUpdateData(data),
14
+ });
15
+
16
+ /**
17
+ * Create a new tier (membership level)
18
+ * @param {Object} tierData - Tier data
19
+ * @param {Object} [options={}] - Options for the API request
20
+ * @returns {Promise<Object>} Created tier
21
+ * @throws {ValidationError} If validation fails or Ghost returns a 422
22
+ * @throws {GhostAPIError} If the API request fails
23
+ */
24
+ export const createTier = service.create;
25
+
26
+ /**
27
+ * Update an existing tier with optimistic concurrency control.
28
+ * @param {string} id - Tier ID
29
+ * @param {Object} updateData - Tier update data
30
+ * @param {Object} [options={}] - Options for the API request
31
+ * @returns {Promise<Object>} Updated tier
32
+ * @throws {ValidationError} If the tier ID is missing or update data is invalid
33
+ * @throws {NotFoundError} If the tier is not found
34
+ * @throws {GhostAPIError} If the API request fails
35
+ */
36
+ export const updateTier = service.update;
37
+
38
+ /**
39
+ * Delete a tier by ID.
40
+ * @param {string} id - Tier ID
41
+ * @returns {Promise<Object>} Deletion result
42
+ * @throws {ValidationError} If the tier ID is missing
43
+ * @throws {NotFoundError} If the tier is not found
44
+ * @throws {GhostAPIError} If the API request fails
45
+ */
46
+ export const deleteTier = service.remove;
47
+
48
+ /**
49
+ * Get all tiers with optional filtering
50
+ * @param {Object} [options={}] - Query options
51
+ * @param {number} [options.limit] - Number of tiers to return (1-100, default 15)
52
+ * @param {number} [options.page] - Page number
53
+ * @param {string} [options.filter] - NQL filter string (e.g., "type:paid", "type:free")
54
+ * @param {string} [options.order] - Order string
55
+ * @param {string} [options.include] - Include string
56
+ * @returns {Promise<Array>} Array of tiers
57
+ * @throws {GhostAPIError} If the API request fails
58
+ */
59
+ export async function getTiers(options = {}) {
60
+ validateTierQueryOptions(options);
61
+ return service.getList(options);
62
+ }
63
+
64
+ /**
65
+ * Get a single tier by ID.
66
+ * @param {string} id - Tier ID
67
+ * @returns {Promise<Object>} Tier object
68
+ * @throws {ValidationError} If the tier ID is missing
69
+ * @throws {NotFoundError} If the tier is not found
70
+ * @throws {GhostAPIError} If the API request fails
71
+ */
72
+ export const getTier = service.getOne;