@jgardner04/ghost-mcp-server 1.5.0 → 1.7.0

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,306 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { mockDotenv } from '../../__tests__/helpers/testUtils.js';
3
+
4
+ // Mock dotenv before other imports
5
+ vi.mock('dotenv', () => mockDotenv());
6
+
7
+ // Create a mock Ghost Admin API
8
+ vi.mock('@tryghost/admin-api', () => {
9
+ const mockNewslettersApi = {
10
+ browse: vi.fn(),
11
+ read: vi.fn(),
12
+ add: vi.fn(),
13
+ edit: vi.fn(),
14
+ delete: vi.fn(),
15
+ };
16
+
17
+ return {
18
+ default: class {
19
+ constructor() {
20
+ return {
21
+ newsletters: mockNewslettersApi,
22
+ };
23
+ }
24
+ },
25
+ mockNewslettersApi,
26
+ };
27
+ });
28
+
29
+ // Import after mocks are set up
30
+ import {
31
+ getNewsletters,
32
+ getNewsletter,
33
+ createNewsletter,
34
+ updateNewsletter,
35
+ deleteNewsletter,
36
+ } from '../ghostServiceImproved.js';
37
+ import { ValidationError, NotFoundError } from '../../errors/index.js';
38
+
39
+ // Get the mock API
40
+ const { mockNewslettersApi } = await vi.importMock('@tryghost/admin-api');
41
+
42
+ describe('ghostServiceImproved - Newsletter Operations', () => {
43
+ beforeEach(() => {
44
+ vi.clearAllMocks();
45
+ });
46
+
47
+ describe('getNewsletters', () => {
48
+ it('should retrieve all newsletters', async () => {
49
+ const mockNewsletters = [
50
+ { id: '1', name: 'Newsletter 1', slug: 'newsletter-1' },
51
+ { id: '2', name: 'Newsletter 2', slug: 'newsletter-2' },
52
+ ];
53
+ mockNewslettersApi.browse.mockResolvedValue(mockNewsletters);
54
+
55
+ const result = await getNewsletters();
56
+
57
+ expect(result).toEqual(mockNewsletters);
58
+ expect(mockNewslettersApi.browse).toHaveBeenCalledWith({ limit: 'all' }, {});
59
+ });
60
+
61
+ it('should support custom limit', async () => {
62
+ const mockNewsletters = [{ id: '1', name: 'Newsletter 1' }];
63
+ mockNewslettersApi.browse.mockResolvedValue(mockNewsletters);
64
+
65
+ await getNewsletters({ limit: 5 });
66
+
67
+ expect(mockNewslettersApi.browse).toHaveBeenCalledWith({ limit: 5 }, {});
68
+ });
69
+
70
+ it('should support filter option', async () => {
71
+ const mockNewsletters = [{ id: '1', name: 'Active Newsletter' }];
72
+ mockNewslettersApi.browse.mockResolvedValue(mockNewsletters);
73
+
74
+ await getNewsletters({ filter: 'status:active' });
75
+
76
+ expect(mockNewslettersApi.browse).toHaveBeenCalledWith(
77
+ { limit: 'all', filter: 'status:active' },
78
+ {}
79
+ );
80
+ });
81
+
82
+ it('should handle empty results', async () => {
83
+ mockNewslettersApi.browse.mockResolvedValue([]);
84
+
85
+ const result = await getNewsletters();
86
+
87
+ expect(result).toEqual([]);
88
+ });
89
+
90
+ it('should propagate API errors', async () => {
91
+ mockNewslettersApi.browse.mockRejectedValue(new Error('API Error'));
92
+
93
+ await expect(getNewsletters()).rejects.toThrow();
94
+ });
95
+ });
96
+
97
+ describe('getNewsletter', () => {
98
+ it('should retrieve a newsletter by ID', async () => {
99
+ const mockNewsletter = { id: 'newsletter-123', name: 'My Newsletter' };
100
+ mockNewslettersApi.read.mockResolvedValue(mockNewsletter);
101
+
102
+ const result = await getNewsletter('newsletter-123');
103
+
104
+ expect(result).toEqual(mockNewsletter);
105
+ expect(mockNewslettersApi.read).toHaveBeenCalledWith({}, { id: 'newsletter-123' });
106
+ });
107
+
108
+ it('should throw ValidationError if ID is missing', async () => {
109
+ await expect(getNewsletter()).rejects.toThrow(ValidationError);
110
+ await expect(getNewsletter()).rejects.toThrow('Newsletter ID is required');
111
+ expect(mockNewslettersApi.read).not.toHaveBeenCalled();
112
+ });
113
+
114
+ it('should throw NotFoundError when newsletter does not exist', async () => {
115
+ const ghostError = new Error('Not found');
116
+ ghostError.response = { status: 404 };
117
+ mockNewslettersApi.read.mockRejectedValue(ghostError);
118
+
119
+ await expect(getNewsletter('nonexistent')).rejects.toThrow(NotFoundError);
120
+ });
121
+ });
122
+
123
+ describe('createNewsletter', () => {
124
+ it('should create a newsletter with valid data', async () => {
125
+ const newsletterData = {
126
+ name: 'Weekly Newsletter',
127
+ description: 'Our weekly updates',
128
+ };
129
+ const createdNewsletter = { id: '1', ...newsletterData };
130
+ mockNewslettersApi.add.mockResolvedValue(createdNewsletter);
131
+
132
+ const result = await createNewsletter(newsletterData);
133
+
134
+ expect(result).toEqual(createdNewsletter);
135
+ expect(mockNewslettersApi.add).toHaveBeenCalledWith(newsletterData, {});
136
+ });
137
+
138
+ it('should create newsletter with sender email', async () => {
139
+ const newsletterData = {
140
+ name: 'Newsletter',
141
+ sender_name: 'John Doe',
142
+ sender_email: 'john@example.com',
143
+ sender_reply_to: 'newsletter',
144
+ };
145
+ const createdNewsletter = { id: '1', ...newsletterData };
146
+ mockNewslettersApi.add.mockResolvedValue(createdNewsletter);
147
+
148
+ const result = await createNewsletter(newsletterData);
149
+
150
+ expect(result).toEqual(createdNewsletter);
151
+ expect(mockNewslettersApi.add).toHaveBeenCalledWith(newsletterData, {});
152
+ });
153
+
154
+ it('should create newsletter with display options', async () => {
155
+ const newsletterData = {
156
+ name: 'Newsletter',
157
+ subscribe_on_signup: true,
158
+ show_header_icon: true,
159
+ show_header_title: false,
160
+ };
161
+ const createdNewsletter = { id: '1', ...newsletterData };
162
+ mockNewslettersApi.add.mockResolvedValue(createdNewsletter);
163
+
164
+ const result = await createNewsletter(newsletterData);
165
+
166
+ expect(result).toEqual(createdNewsletter);
167
+ expect(mockNewslettersApi.add).toHaveBeenCalledWith(newsletterData, {});
168
+ });
169
+
170
+ it('should throw ValidationError if name is missing', async () => {
171
+ const invalidData = { description: 'No name' };
172
+
173
+ await expect(createNewsletter(invalidData)).rejects.toThrow(ValidationError);
174
+ await expect(createNewsletter(invalidData)).rejects.toThrow('Newsletter validation failed');
175
+ expect(mockNewslettersApi.add).not.toHaveBeenCalled();
176
+ });
177
+
178
+ it('should throw ValidationError if name is empty', async () => {
179
+ const invalidData = { name: '' };
180
+
181
+ await expect(createNewsletter(invalidData)).rejects.toThrow(ValidationError);
182
+ await expect(createNewsletter(invalidData)).rejects.toThrow('Newsletter validation failed');
183
+ expect(mockNewslettersApi.add).not.toHaveBeenCalled();
184
+ });
185
+
186
+ it('should handle Ghost API validation errors', async () => {
187
+ const newsletterData = { name: 'Newsletter' };
188
+ const ghostError = new Error('Validation failed');
189
+ ghostError.response = { status: 422 };
190
+ mockNewslettersApi.add.mockRejectedValue(ghostError);
191
+
192
+ await expect(createNewsletter(newsletterData)).rejects.toThrow();
193
+ });
194
+ });
195
+
196
+ describe('updateNewsletter', () => {
197
+ it('should update a newsletter successfully', async () => {
198
+ const existingNewsletter = {
199
+ id: 'newsletter-123',
200
+ name: 'Old Name',
201
+ updated_at: '2024-01-01T00:00:00.000Z',
202
+ };
203
+ const updateData = { name: 'New Name' };
204
+ const updatedNewsletter = { ...existingNewsletter, ...updateData };
205
+
206
+ mockNewslettersApi.read.mockResolvedValue(existingNewsletter);
207
+ mockNewslettersApi.edit.mockResolvedValue(updatedNewsletter);
208
+
209
+ const result = await updateNewsletter('newsletter-123', updateData);
210
+
211
+ expect(result).toEqual(updatedNewsletter);
212
+ expect(mockNewslettersApi.read).toHaveBeenCalledWith({}, { id: 'newsletter-123' });
213
+ expect(mockNewslettersApi.edit).toHaveBeenCalledWith(
214
+ {
215
+ ...existingNewsletter,
216
+ ...updateData,
217
+ },
218
+ { id: 'newsletter-123' }
219
+ );
220
+ });
221
+
222
+ it('should update newsletter with email settings', async () => {
223
+ const existingNewsletter = {
224
+ id: 'newsletter-123',
225
+ name: 'Newsletter',
226
+ updated_at: '2024-01-01T00:00:00.000Z',
227
+ };
228
+ const updateData = {
229
+ sender_name: 'Updated Sender',
230
+ sender_email: 'updated@example.com',
231
+ subscribe_on_signup: false,
232
+ };
233
+
234
+ mockNewslettersApi.read.mockResolvedValue(existingNewsletter);
235
+ mockNewslettersApi.edit.mockResolvedValue({ ...existingNewsletter, ...updateData });
236
+
237
+ await updateNewsletter('newsletter-123', updateData);
238
+
239
+ expect(mockNewslettersApi.edit).toHaveBeenCalledWith(expect.objectContaining(updateData), {
240
+ id: 'newsletter-123',
241
+ });
242
+ });
243
+
244
+ it('should throw ValidationError if ID is missing', async () => {
245
+ await expect(updateNewsletter()).rejects.toThrow(ValidationError);
246
+ await expect(updateNewsletter()).rejects.toThrow('Newsletter ID is required for update');
247
+ expect(mockNewslettersApi.read).not.toHaveBeenCalled();
248
+ });
249
+
250
+ it('should throw NotFoundError if newsletter does not exist', async () => {
251
+ const ghostError = new Error('Not found');
252
+ ghostError.response = { status: 404 };
253
+ mockNewslettersApi.read.mockRejectedValue(ghostError);
254
+
255
+ await expect(updateNewsletter('nonexistent', { name: 'New Name' })).rejects.toThrow(
256
+ NotFoundError
257
+ );
258
+ });
259
+
260
+ it('should preserve updated_at from existing newsletter', async () => {
261
+ const existingNewsletter = {
262
+ id: 'newsletter-123',
263
+ name: 'Newsletter',
264
+ updated_at: '2024-01-01T00:00:00.000Z',
265
+ };
266
+ const updateData = { description: 'Updated description' };
267
+
268
+ mockNewslettersApi.read.mockResolvedValue(existingNewsletter);
269
+ mockNewslettersApi.edit.mockResolvedValue({ ...existingNewsletter, ...updateData });
270
+
271
+ await updateNewsletter('newsletter-123', updateData);
272
+
273
+ expect(mockNewslettersApi.edit).toHaveBeenCalledWith(
274
+ expect.objectContaining({
275
+ updated_at: '2024-01-01T00:00:00.000Z',
276
+ }),
277
+ { id: 'newsletter-123' }
278
+ );
279
+ });
280
+ });
281
+
282
+ describe('deleteNewsletter', () => {
283
+ it('should delete a newsletter successfully', async () => {
284
+ mockNewslettersApi.delete.mockResolvedValue({ id: 'newsletter-123' });
285
+
286
+ const result = await deleteNewsletter('newsletter-123');
287
+
288
+ expect(result).toEqual({ id: 'newsletter-123' });
289
+ expect(mockNewslettersApi.delete).toHaveBeenCalledWith('newsletter-123', {});
290
+ });
291
+
292
+ it('should throw ValidationError if ID is missing', async () => {
293
+ await expect(deleteNewsletter()).rejects.toThrow(ValidationError);
294
+ await expect(deleteNewsletter()).rejects.toThrow('Newsletter ID is required for deletion');
295
+ expect(mockNewslettersApi.delete).not.toHaveBeenCalled();
296
+ });
297
+
298
+ it('should throw NotFoundError if newsletter does not exist', async () => {
299
+ const ghostError = new Error('Not found');
300
+ ghostError.response = { status: 404 };
301
+ mockNewslettersApi.delete.mockRejectedValue(ghostError);
302
+
303
+ await expect(deleteNewsletter('nonexistent')).rejects.toThrow(NotFoundError);
304
+ });
305
+ });
306
+ });
@@ -0,0 +1,217 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { mockDotenv } from '../../__tests__/helpers/testUtils.js';
3
+ import { createMockContextLogger } from '../../__tests__/helpers/mockLogger.js';
4
+
5
+ // Mock dotenv
6
+ vi.mock('dotenv', () => mockDotenv());
7
+
8
+ // Mock logger
9
+ vi.mock('../../utils/logger.js', () => ({
10
+ createContextLogger: createMockContextLogger(),
11
+ }));
12
+
13
+ // Mock ghostServiceImproved functions
14
+ vi.mock('../ghostServiceImproved.js', () => ({
15
+ createNewsletter: vi.fn(),
16
+ }));
17
+
18
+ // Import after mocks are set up
19
+ import { createNewsletterService } from '../newsletterService.js';
20
+ import { createNewsletter } from '../ghostServiceImproved.js';
21
+
22
+ describe('newsletterService', () => {
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ });
26
+
27
+ describe('createNewsletterService - validation', () => {
28
+ it('should accept valid input and create a newsletter', async () => {
29
+ const validInput = {
30
+ name: 'Weekly Newsletter',
31
+ };
32
+ const expectedNewsletter = { id: '1', name: 'Weekly Newsletter', slug: 'weekly-newsletter' };
33
+ createNewsletter.mockResolvedValue(expectedNewsletter);
34
+
35
+ const result = await createNewsletterService(validInput);
36
+
37
+ expect(result).toEqual(expectedNewsletter);
38
+ expect(createNewsletter).toHaveBeenCalledWith(
39
+ expect.objectContaining({
40
+ name: 'Weekly Newsletter',
41
+ })
42
+ );
43
+ });
44
+
45
+ it('should reject input with missing name', async () => {
46
+ const invalidInput = {};
47
+
48
+ await expect(createNewsletterService(invalidInput)).rejects.toThrow(
49
+ 'Invalid newsletter input: "name" is required'
50
+ );
51
+ expect(createNewsletter).not.toHaveBeenCalled();
52
+ });
53
+
54
+ it('should accept all optional fields', async () => {
55
+ const fullInput = {
56
+ name: 'Monthly Newsletter',
57
+ description: 'Our monthly updates',
58
+ sender_name: 'John Doe',
59
+ sender_email: 'john@example.com',
60
+ sender_reply_to: 'newsletter',
61
+ subscribe_on_signup: true,
62
+ show_header_icon: true,
63
+ show_header_title: false,
64
+ };
65
+ const expectedNewsletter = { id: '1', ...fullInput };
66
+ createNewsletter.mockResolvedValue(expectedNewsletter);
67
+
68
+ const result = await createNewsletterService(fullInput);
69
+
70
+ expect(result).toEqual(expectedNewsletter);
71
+ expect(createNewsletter).toHaveBeenCalledWith(expect.objectContaining(fullInput));
72
+ });
73
+
74
+ it('should validate sender_email is a valid email', async () => {
75
+ const invalidInput = {
76
+ name: 'Newsletter',
77
+ sender_email: 'not-an-email',
78
+ };
79
+
80
+ await expect(createNewsletterService(invalidInput)).rejects.toThrow(
81
+ 'Invalid newsletter input:'
82
+ );
83
+ expect(createNewsletter).not.toHaveBeenCalled();
84
+ });
85
+
86
+ it('should accept valid sender_email', async () => {
87
+ const validInput = {
88
+ name: 'Newsletter',
89
+ sender_email: 'valid@example.com',
90
+ };
91
+ createNewsletter.mockResolvedValue({ id: '1', name: 'Newsletter' });
92
+
93
+ await createNewsletterService(validInput);
94
+
95
+ expect(createNewsletter).toHaveBeenCalledWith(
96
+ expect.objectContaining({
97
+ sender_email: 'valid@example.com',
98
+ })
99
+ );
100
+ });
101
+
102
+ it('should validate sender_reply_to enum values', async () => {
103
+ const invalidInput = {
104
+ name: 'Newsletter',
105
+ sender_reply_to: 'invalid',
106
+ };
107
+
108
+ await expect(createNewsletterService(invalidInput)).rejects.toThrow(
109
+ 'Invalid newsletter input:'
110
+ );
111
+ expect(createNewsletter).not.toHaveBeenCalled();
112
+ });
113
+
114
+ it('should accept valid sender_reply_to values', async () => {
115
+ const validValues = ['newsletter', 'support'];
116
+ createNewsletter.mockResolvedValue({ id: '1', name: 'Newsletter' });
117
+
118
+ for (const value of validValues) {
119
+ const input = {
120
+ name: 'Newsletter',
121
+ sender_reply_to: value,
122
+ };
123
+
124
+ await createNewsletterService(input);
125
+
126
+ expect(createNewsletter).toHaveBeenCalledWith(
127
+ expect.objectContaining({ sender_reply_to: value })
128
+ );
129
+ vi.clearAllMocks();
130
+ }
131
+ });
132
+
133
+ it('should validate subscribe_on_signup is boolean', async () => {
134
+ const invalidInput = {
135
+ name: 'Newsletter',
136
+ subscribe_on_signup: 'yes',
137
+ };
138
+
139
+ await expect(createNewsletterService(invalidInput)).rejects.toThrow(
140
+ 'Invalid newsletter input:'
141
+ );
142
+ expect(createNewsletter).not.toHaveBeenCalled();
143
+ });
144
+
145
+ it('should validate show_header_icon is boolean', async () => {
146
+ const invalidInput = {
147
+ name: 'Newsletter',
148
+ show_header_icon: 'true',
149
+ };
150
+
151
+ await expect(createNewsletterService(invalidInput)).rejects.toThrow(
152
+ 'Invalid newsletter input:'
153
+ );
154
+ expect(createNewsletter).not.toHaveBeenCalled();
155
+ });
156
+
157
+ it('should validate show_header_title is boolean', async () => {
158
+ const invalidInput = {
159
+ name: 'Newsletter',
160
+ show_header_title: 1,
161
+ };
162
+
163
+ await expect(createNewsletterService(invalidInput)).rejects.toThrow(
164
+ 'Invalid newsletter input:'
165
+ );
166
+ expect(createNewsletter).not.toHaveBeenCalled();
167
+ });
168
+ });
169
+
170
+ describe('createNewsletterService - defaults and transformations', () => {
171
+ beforeEach(() => {
172
+ createNewsletter.mockResolvedValue({ id: '1', name: 'Newsletter' });
173
+ });
174
+
175
+ it('should pass through all provided fields', async () => {
176
+ const input = {
177
+ name: 'Newsletter',
178
+ description: 'Test description',
179
+ sender_name: 'Sender',
180
+ sender_email: 'sender@example.com',
181
+ sender_reply_to: 'support',
182
+ subscribe_on_signup: false,
183
+ show_header_icon: false,
184
+ show_header_title: true,
185
+ };
186
+
187
+ await createNewsletterService(input);
188
+
189
+ expect(createNewsletter).toHaveBeenCalledWith(expect.objectContaining(input));
190
+ });
191
+
192
+ it('should handle minimal input', async () => {
193
+ const input = {
194
+ name: 'Simple Newsletter',
195
+ };
196
+
197
+ await createNewsletterService(input);
198
+
199
+ expect(createNewsletter).toHaveBeenCalledWith(
200
+ expect.objectContaining({
201
+ name: 'Simple Newsletter',
202
+ })
203
+ );
204
+ });
205
+ });
206
+
207
+ describe('createNewsletterService - error handling', () => {
208
+ it('should propagate errors from ghostServiceImproved', async () => {
209
+ const input = {
210
+ name: 'Newsletter',
211
+ };
212
+ createNewsletter.mockRejectedValue(new Error('Ghost API error'));
213
+
214
+ await expect(createNewsletterService(input)).rejects.toThrow('Ghost API error');
215
+ });
216
+ });
217
+ });
@@ -173,6 +173,27 @@ const validators = {
173
173
  }
174
174
  },
175
175
 
176
+ validateTagUpdateData(updateData) {
177
+ const errors = [];
178
+
179
+ // Name is optional in updates, but if provided, it cannot be empty
180
+ if (updateData.name !== undefined && updateData.name.trim().length === 0) {
181
+ errors.push({ field: 'name', message: 'Tag name cannot be empty' });
182
+ }
183
+
184
+ // Validate slug format if provided
185
+ if (updateData.slug && !/^[a-z0-9-]+$/.test(updateData.slug)) {
186
+ errors.push({
187
+ field: 'slug',
188
+ message: 'Slug must contain only lowercase letters, numbers, and hyphens',
189
+ });
190
+ }
191
+
192
+ if (errors.length > 0) {
193
+ throw new ValidationError('Tag update validation failed', errors);
194
+ }
195
+ },
196
+
176
197
  async validateImagePath(imagePath) {
177
198
  if (!imagePath || typeof imagePath !== 'string') {
178
199
  throw new ValidationError('Image path is required and must be a string');
@@ -224,6 +245,18 @@ const validators = {
224
245
  throw new ValidationError('Page validation failed', errors);
225
246
  }
226
247
  },
248
+
249
+ validateNewsletterData(newsletterData) {
250
+ const errors = [];
251
+
252
+ if (!newsletterData.name || newsletterData.name.trim().length === 0) {
253
+ errors.push({ field: 'name', message: 'Newsletter name is required' });
254
+ }
255
+
256
+ if (errors.length > 0) {
257
+ throw new ValidationError('Newsletter validation failed', errors);
258
+ }
259
+ },
227
260
  };
228
261
 
229
262
  /**
@@ -681,13 +714,13 @@ export async function getTags(name) {
681
714
  }
682
715
  }
683
716
 
684
- export async function getTag(tagId) {
717
+ export async function getTag(tagId, options = {}) {
685
718
  if (!tagId) {
686
719
  throw new ValidationError('Tag ID is required');
687
720
  }
688
721
 
689
722
  try {
690
- return await handleApiRequest('tags', 'read', { id: tagId });
723
+ return await handleApiRequest('tags', 'read', { id: tagId }, options);
691
724
  } catch (error) {
692
725
  if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
693
726
  throw new NotFoundError('Tag', tagId);
@@ -701,7 +734,7 @@ export async function updateTag(tagId, updateData) {
701
734
  throw new ValidationError('Tag ID is required for update');
702
735
  }
703
736
 
704
- validators.validateTagData({ name: 'dummy', ...updateData }); // Validate update data
737
+ validators.validateTagUpdateData(updateData); // Validate update data
705
738
 
706
739
  try {
707
740
  const existingTag = await getTag(tagId);
@@ -739,6 +772,103 @@ export async function deleteTag(tagId) {
739
772
  }
740
773
  }
741
774
 
775
+ /**
776
+ * Newsletter CRUD Operations
777
+ */
778
+
779
+ export async function getNewsletters(options = {}) {
780
+ const defaultOptions = {
781
+ limit: 'all',
782
+ ...options,
783
+ };
784
+
785
+ try {
786
+ const newsletters = await handleApiRequest('newsletters', 'browse', {}, defaultOptions);
787
+ return newsletters || [];
788
+ } catch (error) {
789
+ console.error('Failed to get newsletters:', error);
790
+ throw error;
791
+ }
792
+ }
793
+
794
+ export async function getNewsletter(newsletterId) {
795
+ if (!newsletterId) {
796
+ throw new ValidationError('Newsletter ID is required');
797
+ }
798
+
799
+ try {
800
+ return await handleApiRequest('newsletters', 'read', { id: newsletterId });
801
+ } catch (error) {
802
+ if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
803
+ throw new NotFoundError('Newsletter', newsletterId);
804
+ }
805
+ throw error;
806
+ }
807
+ }
808
+
809
+ export async function createNewsletter(newsletterData) {
810
+ // Validate input
811
+ validators.validateNewsletterData(newsletterData);
812
+
813
+ try {
814
+ return await handleApiRequest('newsletters', 'add', newsletterData);
815
+ } catch (error) {
816
+ if (error instanceof GhostAPIError && error.ghostStatusCode === 422) {
817
+ throw new ValidationError('Newsletter creation failed', [
818
+ { field: 'newsletter', message: error.originalError },
819
+ ]);
820
+ }
821
+ throw error;
822
+ }
823
+ }
824
+
825
+ export async function updateNewsletter(newsletterId, updateData) {
826
+ if (!newsletterId) {
827
+ throw new ValidationError('Newsletter ID is required for update');
828
+ }
829
+
830
+ try {
831
+ // Get existing newsletter to retrieve updated_at for conflict resolution
832
+ const existingNewsletter = await handleApiRequest('newsletters', 'read', {
833
+ id: newsletterId,
834
+ });
835
+
836
+ // Merge existing data with updates, preserving updated_at
837
+ const mergedData = {
838
+ ...existingNewsletter,
839
+ ...updateData,
840
+ updated_at: existingNewsletter.updated_at,
841
+ };
842
+
843
+ return await handleApiRequest('newsletters', 'edit', mergedData, { id: newsletterId });
844
+ } catch (error) {
845
+ if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
846
+ throw new NotFoundError('Newsletter', newsletterId);
847
+ }
848
+ if (error instanceof GhostAPIError && error.ghostStatusCode === 422) {
849
+ throw new ValidationError('Newsletter update failed', [
850
+ { field: 'newsletter', message: error.originalError },
851
+ ]);
852
+ }
853
+ throw error;
854
+ }
855
+ }
856
+
857
+ export async function deleteNewsletter(newsletterId) {
858
+ if (!newsletterId) {
859
+ throw new ValidationError('Newsletter ID is required for deletion');
860
+ }
861
+
862
+ try {
863
+ return await handleApiRequest('newsletters', 'delete', newsletterId);
864
+ } catch (error) {
865
+ if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
866
+ throw new NotFoundError('Newsletter', newsletterId);
867
+ }
868
+ throw error;
869
+ }
870
+ }
871
+
742
872
  /**
743
873
  * Health check for Ghost API connection
744
874
  */
@@ -790,5 +920,10 @@ export default {
790
920
  getTag,
791
921
  updateTag,
792
922
  deleteTag,
923
+ getNewsletters,
924
+ getNewsletter,
925
+ createNewsletter,
926
+ updateNewsletter,
927
+ deleteNewsletter,
793
928
  checkHealth,
794
929
  };