@jgardner04/ghost-mcp-server 1.9.0 → 1.11.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.
- package/package.json +1 -1
- package/src/mcp_server_improved.js +300 -2
- package/src/services/__tests__/ghostServiceImproved.members.test.js +288 -1
- package/src/services/__tests__/ghostServiceImproved.tiers.test.js +392 -0
- package/src/services/__tests__/memberService.test.js +233 -1
- package/src/services/__tests__/tierService.test.js +372 -0
- package/src/services/ghostServiceImproved.js +238 -0
- package/src/services/memberService.js +190 -0
- package/src/services/tierService.js +304 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
validateTierData,
|
|
4
|
+
validateTierUpdateData,
|
|
5
|
+
validateTierQueryOptions,
|
|
6
|
+
sanitizeNqlValue,
|
|
7
|
+
} from '../tierService.js';
|
|
8
|
+
import { ValidationError } from '../../errors/index.js';
|
|
9
|
+
|
|
10
|
+
describe('tierService - Validation', () => {
|
|
11
|
+
describe('validateTierData', () => {
|
|
12
|
+
it('should validate required name field', () => {
|
|
13
|
+
expect(() => validateTierData({})).toThrow(ValidationError);
|
|
14
|
+
expect(() => validateTierData({})).toThrow('Tier validation failed');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should validate required currency field', () => {
|
|
18
|
+
expect(() => validateTierData({ name: 'Premium' })).toThrow(ValidationError);
|
|
19
|
+
expect(() => validateTierData({ name: 'Premium' })).toThrow('Tier validation failed');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should accept valid tier data with name and currency', () => {
|
|
23
|
+
expect(() =>
|
|
24
|
+
validateTierData({
|
|
25
|
+
name: 'Premium',
|
|
26
|
+
currency: 'USD',
|
|
27
|
+
})
|
|
28
|
+
).not.toThrow();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should validate name is a non-empty string', () => {
|
|
32
|
+
expect(() =>
|
|
33
|
+
validateTierData({
|
|
34
|
+
name: '',
|
|
35
|
+
currency: 'USD',
|
|
36
|
+
})
|
|
37
|
+
).toThrow('Tier validation failed');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should validate name does not exceed max length', () => {
|
|
41
|
+
const longName = 'a'.repeat(192);
|
|
42
|
+
expect(() =>
|
|
43
|
+
validateTierData({
|
|
44
|
+
name: longName,
|
|
45
|
+
currency: 'USD',
|
|
46
|
+
})
|
|
47
|
+
).toThrow('Tier validation failed');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should validate currency is a 3-letter uppercase code', () => {
|
|
51
|
+
expect(() =>
|
|
52
|
+
validateTierData({
|
|
53
|
+
name: 'Premium',
|
|
54
|
+
currency: 'us',
|
|
55
|
+
})
|
|
56
|
+
).toThrow('Tier validation failed');
|
|
57
|
+
|
|
58
|
+
expect(() =>
|
|
59
|
+
validateTierData({
|
|
60
|
+
name: 'Premium',
|
|
61
|
+
currency: 'USDD',
|
|
62
|
+
})
|
|
63
|
+
).toThrow('Tier validation failed');
|
|
64
|
+
|
|
65
|
+
expect(() =>
|
|
66
|
+
validateTierData({
|
|
67
|
+
name: 'Premium',
|
|
68
|
+
currency: '123',
|
|
69
|
+
})
|
|
70
|
+
).toThrow('Tier validation failed');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should validate description does not exceed max length', () => {
|
|
74
|
+
const longDescription = 'a'.repeat(2001);
|
|
75
|
+
expect(() =>
|
|
76
|
+
validateTierData({
|
|
77
|
+
name: 'Premium',
|
|
78
|
+
currency: 'USD',
|
|
79
|
+
description: longDescription,
|
|
80
|
+
})
|
|
81
|
+
).toThrow('Tier validation failed');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should validate monthly_price is a non-negative number', () => {
|
|
85
|
+
expect(() =>
|
|
86
|
+
validateTierData({
|
|
87
|
+
name: 'Premium',
|
|
88
|
+
currency: 'USD',
|
|
89
|
+
monthly_price: -100,
|
|
90
|
+
})
|
|
91
|
+
).toThrow('Tier validation failed');
|
|
92
|
+
|
|
93
|
+
expect(() =>
|
|
94
|
+
validateTierData({
|
|
95
|
+
name: 'Premium',
|
|
96
|
+
currency: 'USD',
|
|
97
|
+
monthly_price: 'invalid',
|
|
98
|
+
})
|
|
99
|
+
).toThrow('Tier validation failed');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should validate yearly_price is a non-negative number', () => {
|
|
103
|
+
expect(() =>
|
|
104
|
+
validateTierData({
|
|
105
|
+
name: 'Premium',
|
|
106
|
+
currency: 'USD',
|
|
107
|
+
yearly_price: -1000,
|
|
108
|
+
})
|
|
109
|
+
).toThrow('Tier validation failed');
|
|
110
|
+
|
|
111
|
+
expect(() =>
|
|
112
|
+
validateTierData({
|
|
113
|
+
name: 'Premium',
|
|
114
|
+
currency: 'USD',
|
|
115
|
+
yearly_price: 'invalid',
|
|
116
|
+
})
|
|
117
|
+
).toThrow('Tier validation failed');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should validate benefits is an array of strings', () => {
|
|
121
|
+
expect(() =>
|
|
122
|
+
validateTierData({
|
|
123
|
+
name: 'Premium',
|
|
124
|
+
currency: 'USD',
|
|
125
|
+
benefits: 'not an array',
|
|
126
|
+
})
|
|
127
|
+
).toThrow('Tier validation failed');
|
|
128
|
+
|
|
129
|
+
expect(() =>
|
|
130
|
+
validateTierData({
|
|
131
|
+
name: 'Premium',
|
|
132
|
+
currency: 'USD',
|
|
133
|
+
benefits: [123, 456],
|
|
134
|
+
})
|
|
135
|
+
).toThrow('Tier validation failed');
|
|
136
|
+
|
|
137
|
+
expect(() =>
|
|
138
|
+
validateTierData({
|
|
139
|
+
name: 'Premium',
|
|
140
|
+
currency: 'USD',
|
|
141
|
+
benefits: ['Benefit 1', ''],
|
|
142
|
+
})
|
|
143
|
+
).toThrow('Tier validation failed');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should validate welcome_page_url is a valid URL', () => {
|
|
147
|
+
expect(() =>
|
|
148
|
+
validateTierData({
|
|
149
|
+
name: 'Premium',
|
|
150
|
+
currency: 'USD',
|
|
151
|
+
welcome_page_url: 'not-a-url',
|
|
152
|
+
})
|
|
153
|
+
).toThrow('Tier validation failed');
|
|
154
|
+
|
|
155
|
+
expect(() =>
|
|
156
|
+
validateTierData({
|
|
157
|
+
name: 'Premium',
|
|
158
|
+
currency: 'USD',
|
|
159
|
+
welcome_page_url: 'ftp://example.com',
|
|
160
|
+
})
|
|
161
|
+
).toThrow('Tier validation failed');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should accept valid welcome_page_url', () => {
|
|
165
|
+
expect(() =>
|
|
166
|
+
validateTierData({
|
|
167
|
+
name: 'Premium',
|
|
168
|
+
currency: 'USD',
|
|
169
|
+
welcome_page_url: 'https://example.com/welcome',
|
|
170
|
+
})
|
|
171
|
+
).not.toThrow();
|
|
172
|
+
|
|
173
|
+
expect(() =>
|
|
174
|
+
validateTierData({
|
|
175
|
+
name: 'Premium',
|
|
176
|
+
currency: 'USD',
|
|
177
|
+
welcome_page_url: 'http://example.com/welcome',
|
|
178
|
+
})
|
|
179
|
+
).not.toThrow();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should accept complete valid tier data', () => {
|
|
183
|
+
expect(() =>
|
|
184
|
+
validateTierData({
|
|
185
|
+
name: 'Premium Membership',
|
|
186
|
+
description: 'Access to premium content',
|
|
187
|
+
currency: 'USD',
|
|
188
|
+
monthly_price: 999,
|
|
189
|
+
yearly_price: 9999,
|
|
190
|
+
benefits: ['Ad-free experience', 'Exclusive content', 'Priority support'],
|
|
191
|
+
welcome_page_url: 'https://example.com/welcome',
|
|
192
|
+
})
|
|
193
|
+
).not.toThrow();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('validateTierUpdateData', () => {
|
|
198
|
+
it('should accept empty update data', () => {
|
|
199
|
+
expect(() => validateTierUpdateData({})).not.toThrow();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should validate name if provided', () => {
|
|
203
|
+
expect(() =>
|
|
204
|
+
validateTierUpdateData({
|
|
205
|
+
name: '',
|
|
206
|
+
})
|
|
207
|
+
).toThrow('Tier validation failed');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should validate currency if provided', () => {
|
|
211
|
+
expect(() =>
|
|
212
|
+
validateTierUpdateData({
|
|
213
|
+
currency: 'us',
|
|
214
|
+
})
|
|
215
|
+
).toThrow('Tier validation failed');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should validate description length if provided', () => {
|
|
219
|
+
const longDescription = 'a'.repeat(2001);
|
|
220
|
+
expect(() =>
|
|
221
|
+
validateTierUpdateData({
|
|
222
|
+
description: longDescription,
|
|
223
|
+
})
|
|
224
|
+
).toThrow('Tier validation failed');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should validate monthly_price if provided', () => {
|
|
228
|
+
expect(() =>
|
|
229
|
+
validateTierUpdateData({
|
|
230
|
+
monthly_price: -100,
|
|
231
|
+
})
|
|
232
|
+
).toThrow('Tier validation failed');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should validate yearly_price if provided', () => {
|
|
236
|
+
expect(() =>
|
|
237
|
+
validateTierUpdateData({
|
|
238
|
+
yearly_price: -1000,
|
|
239
|
+
})
|
|
240
|
+
).toThrow('Tier validation failed');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should validate benefits if provided', () => {
|
|
244
|
+
expect(() =>
|
|
245
|
+
validateTierUpdateData({
|
|
246
|
+
benefits: 'not an array',
|
|
247
|
+
})
|
|
248
|
+
).toThrow('Tier validation failed');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should validate welcome_page_url if provided', () => {
|
|
252
|
+
expect(() =>
|
|
253
|
+
validateTierUpdateData({
|
|
254
|
+
welcome_page_url: 'not-a-url',
|
|
255
|
+
})
|
|
256
|
+
).toThrow('Tier validation failed');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should accept valid update data', () => {
|
|
260
|
+
expect(() =>
|
|
261
|
+
validateTierUpdateData({
|
|
262
|
+
name: 'Updated Premium',
|
|
263
|
+
monthly_price: 1299,
|
|
264
|
+
benefits: ['New benefit'],
|
|
265
|
+
})
|
|
266
|
+
).not.toThrow();
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('validateTierQueryOptions', () => {
|
|
271
|
+
it('should accept empty options', () => {
|
|
272
|
+
expect(() => validateTierQueryOptions({})).not.toThrow();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should validate limit is within range', () => {
|
|
276
|
+
expect(() => validateTierQueryOptions({ limit: 0 })).toThrow('Tier query validation failed');
|
|
277
|
+
|
|
278
|
+
expect(() => validateTierQueryOptions({ limit: 101 })).toThrow(
|
|
279
|
+
'Tier query validation failed'
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
expect(() => validateTierQueryOptions({ limit: 50 })).not.toThrow();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should validate limit is a number', () => {
|
|
286
|
+
expect(() => validateTierQueryOptions({ limit: 'invalid' })).toThrow(
|
|
287
|
+
'Tier query validation failed'
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should validate page is >= 1', () => {
|
|
292
|
+
expect(() => validateTierQueryOptions({ page: 0 })).toThrow('Tier query validation failed');
|
|
293
|
+
|
|
294
|
+
expect(() => validateTierQueryOptions({ page: -1 })).toThrow('Tier query validation failed');
|
|
295
|
+
|
|
296
|
+
expect(() => validateTierQueryOptions({ page: 1 })).not.toThrow();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should validate page is a number', () => {
|
|
300
|
+
expect(() => validateTierQueryOptions({ page: 'invalid' })).toThrow(
|
|
301
|
+
'Tier query validation failed'
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should validate filter is a non-empty string', () => {
|
|
306
|
+
expect(() => validateTierQueryOptions({ filter: '' })).toThrow(
|
|
307
|
+
'Tier query validation failed'
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
expect(() => validateTierQueryOptions({ filter: ' ' })).toThrow(
|
|
311
|
+
'Tier query validation failed'
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
expect(() => validateTierQueryOptions({ filter: 'type:paid' })).not.toThrow();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should validate order is a non-empty string', () => {
|
|
318
|
+
expect(() => validateTierQueryOptions({ order: '' })).toThrow('Tier query validation failed');
|
|
319
|
+
|
|
320
|
+
expect(() => validateTierQueryOptions({ order: 'created_at desc' })).not.toThrow();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should validate include is a non-empty string', () => {
|
|
324
|
+
expect(() => validateTierQueryOptions({ include: '' })).toThrow(
|
|
325
|
+
'Tier query validation failed'
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
expect(() =>
|
|
329
|
+
validateTierQueryOptions({ include: 'monthly_price,yearly_price' })
|
|
330
|
+
).not.toThrow();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should accept valid query options', () => {
|
|
334
|
+
expect(() =>
|
|
335
|
+
validateTierQueryOptions({
|
|
336
|
+
limit: 50,
|
|
337
|
+
page: 2,
|
|
338
|
+
filter: 'type:paid',
|
|
339
|
+
order: 'created_at desc',
|
|
340
|
+
include: 'monthly_price,yearly_price',
|
|
341
|
+
})
|
|
342
|
+
).not.toThrow();
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe('sanitizeNqlValue', () => {
|
|
347
|
+
it('should return value if undefined or null', () => {
|
|
348
|
+
expect(sanitizeNqlValue(null)).toBe(null);
|
|
349
|
+
expect(sanitizeNqlValue(undefined)).toBe(undefined);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should escape backslashes', () => {
|
|
353
|
+
expect(sanitizeNqlValue('test\\value')).toBe('test\\\\value');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should escape single quotes', () => {
|
|
357
|
+
expect(sanitizeNqlValue("test'value")).toBe("test\\'value");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should escape double quotes', () => {
|
|
361
|
+
expect(sanitizeNqlValue('test"value')).toBe('test\\"value');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should escape multiple special characters', () => {
|
|
365
|
+
expect(sanitizeNqlValue('test\\value"with\'quotes')).toBe('test\\\\value\\"with\\\'quotes');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should handle strings without special characters', () => {
|
|
369
|
+
expect(sanitizeNqlValue('simple-value')).toBe('simple-value');
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
});
|
|
@@ -876,6 +876,111 @@ export async function deleteMember(memberId) {
|
|
|
876
876
|
}
|
|
877
877
|
}
|
|
878
878
|
|
|
879
|
+
/**
|
|
880
|
+
* List members from Ghost CMS with optional filtering and pagination
|
|
881
|
+
* @param {Object} [options] - Query options
|
|
882
|
+
* @param {number} [options.limit] - Number of members to return (1-100)
|
|
883
|
+
* @param {number} [options.page] - Page number (1+)
|
|
884
|
+
* @param {string} [options.filter] - NQL filter string (e.g., 'status:paid')
|
|
885
|
+
* @param {string} [options.order] - Order string (e.g., 'created_at desc')
|
|
886
|
+
* @param {string} [options.include] - Include string (e.g., 'labels,newsletters')
|
|
887
|
+
* @returns {Promise<Array>} Array of member objects
|
|
888
|
+
* @throws {ValidationError} If validation fails
|
|
889
|
+
* @throws {GhostAPIError} If the API request fails
|
|
890
|
+
*/
|
|
891
|
+
export async function getMembers(options = {}) {
|
|
892
|
+
// Import and validate query options
|
|
893
|
+
const { validateMemberQueryOptions } = await import('./memberService.js');
|
|
894
|
+
validateMemberQueryOptions(options);
|
|
895
|
+
|
|
896
|
+
const defaultOptions = {
|
|
897
|
+
limit: 15,
|
|
898
|
+
...options,
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
try {
|
|
902
|
+
const members = await handleApiRequest('members', 'browse', {}, defaultOptions);
|
|
903
|
+
return members || [];
|
|
904
|
+
} catch (error) {
|
|
905
|
+
console.error('Failed to get members:', error);
|
|
906
|
+
throw error;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Get a single member from Ghost CMS by ID or email
|
|
912
|
+
* @param {Object} params - Lookup parameters (id OR email required)
|
|
913
|
+
* @param {string} [params.id] - Member ID
|
|
914
|
+
* @param {string} [params.email] - Member email
|
|
915
|
+
* @returns {Promise<Object>} The member object
|
|
916
|
+
* @throws {ValidationError} If validation fails
|
|
917
|
+
* @throws {NotFoundError} If the member is not found
|
|
918
|
+
* @throws {GhostAPIError} If the API request fails
|
|
919
|
+
*/
|
|
920
|
+
export async function getMember(params) {
|
|
921
|
+
// Import and validate lookup parameters
|
|
922
|
+
const { validateMemberLookup, sanitizeNqlValue } = await import('./memberService.js');
|
|
923
|
+
const { lookupType, id, email } = validateMemberLookup(params);
|
|
924
|
+
|
|
925
|
+
try {
|
|
926
|
+
if (lookupType === 'id') {
|
|
927
|
+
// Lookup by ID using read endpoint
|
|
928
|
+
return await handleApiRequest('members', 'read', { id }, { id });
|
|
929
|
+
} else {
|
|
930
|
+
// Lookup by email using browse with filter
|
|
931
|
+
const sanitizedEmail = sanitizeNqlValue(email);
|
|
932
|
+
const members = await handleApiRequest(
|
|
933
|
+
'members',
|
|
934
|
+
'browse',
|
|
935
|
+
{},
|
|
936
|
+
{ filter: `email:'${sanitizedEmail}'`, limit: 1 }
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
if (!members || members.length === 0) {
|
|
940
|
+
throw new NotFoundError('Member', email);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return members[0];
|
|
944
|
+
}
|
|
945
|
+
} catch (error) {
|
|
946
|
+
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
947
|
+
throw new NotFoundError('Member', id || email);
|
|
948
|
+
}
|
|
949
|
+
throw error;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Search members by name or email
|
|
955
|
+
* @param {string} query - Search query (searches name and email fields)
|
|
956
|
+
* @param {Object} [options] - Additional options
|
|
957
|
+
* @param {number} [options.limit] - Maximum number of results (default: 15)
|
|
958
|
+
* @returns {Promise<Array>} Array of matching member objects
|
|
959
|
+
* @throws {ValidationError} If validation fails
|
|
960
|
+
* @throws {GhostAPIError} If the API request fails
|
|
961
|
+
*/
|
|
962
|
+
export async function searchMembers(query, options = {}) {
|
|
963
|
+
// Import and validate search query and options
|
|
964
|
+
const { validateSearchQuery, validateSearchOptions, sanitizeNqlValue } =
|
|
965
|
+
await import('./memberService.js');
|
|
966
|
+
validateSearchOptions(options);
|
|
967
|
+
const sanitizedQuery = sanitizeNqlValue(validateSearchQuery(query));
|
|
968
|
+
|
|
969
|
+
const limit = options.limit || 15;
|
|
970
|
+
|
|
971
|
+
// Build NQL filter for name or email containing the query
|
|
972
|
+
// Ghost uses ~ for contains/like matching
|
|
973
|
+
const filter = `name:~'${sanitizedQuery}',email:~'${sanitizedQuery}'`;
|
|
974
|
+
|
|
975
|
+
try {
|
|
976
|
+
const members = await handleApiRequest('members', 'browse', {}, { filter, limit });
|
|
977
|
+
return members || [];
|
|
978
|
+
} catch (error) {
|
|
979
|
+
console.error('Failed to search members:', error);
|
|
980
|
+
throw error;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
879
984
|
/**
|
|
880
985
|
* Newsletter CRUD Operations
|
|
881
986
|
*/
|
|
@@ -973,6 +1078,131 @@ export async function deleteNewsletter(newsletterId) {
|
|
|
973
1078
|
}
|
|
974
1079
|
}
|
|
975
1080
|
|
|
1081
|
+
/**
|
|
1082
|
+
* Create a new tier (membership level)
|
|
1083
|
+
* @param {Object} tierData - Tier data
|
|
1084
|
+
* @param {Object} [options={}] - Options for the API request
|
|
1085
|
+
* @returns {Promise<Object>} Created tier
|
|
1086
|
+
*/
|
|
1087
|
+
export async function createTier(tierData, options = {}) {
|
|
1088
|
+
const { validateTierData } = await import('./tierService.js');
|
|
1089
|
+
validateTierData(tierData);
|
|
1090
|
+
|
|
1091
|
+
try {
|
|
1092
|
+
return await handleApiRequest('tiers', 'add', tierData, options);
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
if (error instanceof GhostAPIError && error.ghostStatusCode === 422) {
|
|
1095
|
+
throw new ValidationError('Tier creation failed due to validation errors', [
|
|
1096
|
+
{ field: 'tier', message: error.originalError },
|
|
1097
|
+
]);
|
|
1098
|
+
}
|
|
1099
|
+
throw error;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* Update an existing tier
|
|
1105
|
+
* @param {string} id - Tier ID
|
|
1106
|
+
* @param {Object} updateData - Tier update data
|
|
1107
|
+
* @param {Object} [options={}] - Options for the API request
|
|
1108
|
+
* @returns {Promise<Object>} Updated tier
|
|
1109
|
+
*/
|
|
1110
|
+
export async function updateTier(id, updateData, options = {}) {
|
|
1111
|
+
if (!id || typeof id !== 'string' || id.trim().length === 0) {
|
|
1112
|
+
throw new ValidationError('Tier ID is required for update');
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const { validateTierUpdateData } = await import('./tierService.js');
|
|
1116
|
+
validateTierUpdateData(updateData);
|
|
1117
|
+
|
|
1118
|
+
try {
|
|
1119
|
+
// Get existing tier for merge
|
|
1120
|
+
const existingTier = await handleApiRequest('tiers', 'read', { id }, { id });
|
|
1121
|
+
|
|
1122
|
+
// Merge updates with existing data
|
|
1123
|
+
const mergedData = {
|
|
1124
|
+
...existingTier,
|
|
1125
|
+
...updateData,
|
|
1126
|
+
updated_at: existingTier.updated_at,
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
return await handleApiRequest('tiers', 'edit', mergedData, { id, ...options });
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
1132
|
+
throw new NotFoundError('Tier', id);
|
|
1133
|
+
}
|
|
1134
|
+
throw error;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Delete a tier
|
|
1140
|
+
* @param {string} id - Tier ID
|
|
1141
|
+
* @returns {Promise<Object>} Deletion result
|
|
1142
|
+
*/
|
|
1143
|
+
export async function deleteTier(id) {
|
|
1144
|
+
if (!id || typeof id !== 'string' || id.trim().length === 0) {
|
|
1145
|
+
throw new ValidationError('Tier ID is required for deletion');
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
try {
|
|
1149
|
+
return await handleApiRequest('tiers', 'delete', { id });
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
1152
|
+
throw new NotFoundError('Tier', id);
|
|
1153
|
+
}
|
|
1154
|
+
throw error;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Get all tiers with optional filtering
|
|
1160
|
+
* @param {Object} [options={}] - Query options
|
|
1161
|
+
* @param {number} [options.limit] - Number of tiers to return (1-100, default 15)
|
|
1162
|
+
* @param {number} [options.page] - Page number
|
|
1163
|
+
* @param {string} [options.filter] - NQL filter string (e.g., "type:paid", "type:free")
|
|
1164
|
+
* @param {string} [options.order] - Order string
|
|
1165
|
+
* @param {string} [options.include] - Include string
|
|
1166
|
+
* @returns {Promise<Array>} Array of tiers
|
|
1167
|
+
*/
|
|
1168
|
+
export async function getTiers(options = {}) {
|
|
1169
|
+
const { validateTierQueryOptions } = await import('./tierService.js');
|
|
1170
|
+
validateTierQueryOptions(options);
|
|
1171
|
+
|
|
1172
|
+
const defaultOptions = {
|
|
1173
|
+
limit: 15,
|
|
1174
|
+
...options,
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
try {
|
|
1178
|
+
const tiers = await handleApiRequest('tiers', 'browse', {}, defaultOptions);
|
|
1179
|
+
return tiers || [];
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
console.error('Failed to get tiers:', error);
|
|
1182
|
+
throw error;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Get a single tier by ID
|
|
1188
|
+
* @param {string} id - Tier ID
|
|
1189
|
+
* @returns {Promise<Object>} Tier object
|
|
1190
|
+
*/
|
|
1191
|
+
export async function getTier(id) {
|
|
1192
|
+
if (!id || typeof id !== 'string' || id.trim().length === 0) {
|
|
1193
|
+
throw new ValidationError('Tier ID is required and must be a non-empty string');
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
try {
|
|
1197
|
+
return await handleApiRequest('tiers', 'read', { id }, { id });
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
if (error instanceof GhostAPIError && error.ghostStatusCode === 404) {
|
|
1200
|
+
throw new NotFoundError('Tier', id);
|
|
1201
|
+
}
|
|
1202
|
+
throw error;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
976
1206
|
/**
|
|
977
1207
|
* Health check for Ghost API connection
|
|
978
1208
|
*/
|
|
@@ -1027,10 +1257,18 @@ export default {
|
|
|
1027
1257
|
createMember,
|
|
1028
1258
|
updateMember,
|
|
1029
1259
|
deleteMember,
|
|
1260
|
+
getMembers,
|
|
1261
|
+
getMember,
|
|
1262
|
+
searchMembers,
|
|
1030
1263
|
getNewsletters,
|
|
1031
1264
|
getNewsletter,
|
|
1032
1265
|
createNewsletter,
|
|
1033
1266
|
updateNewsletter,
|
|
1034
1267
|
deleteNewsletter,
|
|
1268
|
+
createTier,
|
|
1269
|
+
updateTier,
|
|
1270
|
+
deleteTier,
|
|
1271
|
+
getTiers,
|
|
1272
|
+
getTier,
|
|
1035
1273
|
checkHealth,
|
|
1036
1274
|
};
|