@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,218 @@
1
+ import { promises as fs } from 'fs';
2
+ import { ValidationError, NotFoundError } from '../errors/index.js';
3
+
4
+ /**
5
+ * Input validation helpers
6
+ */
7
+ export const validators = {
8
+ /**
9
+ * Validates that an ID is a non-empty string. Used by CRUD helpers to enforce
10
+ * consistent ID validation across all resource types.
11
+ * @param {string} id - The resource ID to validate
12
+ * @param {string} entityName - Human-readable resource name for error messages (e.g., 'Post', 'Tag')
13
+ * @throws {ValidationError} If the ID is falsy, not a string, or empty/whitespace
14
+ */
15
+ requireId(id, entityName) {
16
+ if (!id || typeof id !== 'string' || id.trim().length === 0) {
17
+ throw new ValidationError(`${entityName} ID is required`);
18
+ }
19
+ },
20
+
21
+ /**
22
+ * Validates scheduling fields for posts and pages. Ensures published_at is present
23
+ * when status is 'scheduled' and that the date is valid and in the future.
24
+ * @param {Object} data - The resource data containing status and/or published_at
25
+ * @param {string} [data.status] - Resource status ('draft', 'published', 'scheduled')
26
+ * @param {string} [data.published_at] - ISO 8601 date string for scheduling
27
+ * @param {string} [resourceLabel='Resource'] - Human-readable label for error messages
28
+ * @throws {ValidationError} If scheduling validation fails
29
+ */
30
+ validateScheduledStatus(data, resourceLabel = 'Resource') {
31
+ const errors = [];
32
+
33
+ if (data.status === 'scheduled' && !data.published_at) {
34
+ errors.push({
35
+ field: 'published_at',
36
+ message: 'published_at is required when status is scheduled',
37
+ });
38
+ }
39
+
40
+ if (data.published_at) {
41
+ const publishDate = new Date(data.published_at);
42
+ if (isNaN(publishDate.getTime())) {
43
+ errors.push({ field: 'published_at', message: 'Invalid date format' });
44
+ } else if (data.status === 'scheduled' && publishDate <= new Date()) {
45
+ errors.push({ field: 'published_at', message: 'Scheduled date must be in the future' });
46
+ }
47
+ }
48
+
49
+ if (errors.length > 0) {
50
+ throw new ValidationError(`${resourceLabel} validation failed`, errors);
51
+ }
52
+ },
53
+
54
+ /**
55
+ * Validates post creation data. Requires a title and either html or mobiledoc content.
56
+ * Also validates status values and delegates to validateScheduledStatus for scheduling.
57
+ * @param {Object} postData - The post data to validate
58
+ * @param {string} postData.title - Post title (required, non-empty)
59
+ * @param {string} [postData.html] - HTML content (required if mobiledoc not provided)
60
+ * @param {string} [postData.mobiledoc] - Mobiledoc content (required if html not provided)
61
+ * @param {string} [postData.status] - Post status ('draft', 'published', 'scheduled')
62
+ * @param {string} [postData.published_at] - ISO 8601 date for scheduled posts
63
+ * @throws {ValidationError} If validation fails
64
+ */
65
+ validatePostData(postData) {
66
+ const errors = [];
67
+
68
+ if (!postData.title || postData.title.trim().length === 0) {
69
+ errors.push({ field: 'title', message: 'Title is required' });
70
+ }
71
+
72
+ if (!postData.html && !postData.mobiledoc) {
73
+ errors.push({ field: 'content', message: 'Either html or mobiledoc content is required' });
74
+ }
75
+
76
+ if (postData.status && !['draft', 'published', 'scheduled'].includes(postData.status)) {
77
+ errors.push({
78
+ field: 'status',
79
+ message: 'Invalid status. Must be draft, published, or scheduled',
80
+ });
81
+ }
82
+
83
+ if (errors.length > 0) {
84
+ throw new ValidationError('Post validation failed', errors);
85
+ }
86
+
87
+ this.validateScheduledStatus(postData, 'Post');
88
+ },
89
+
90
+ /**
91
+ * Validates tag creation data. Requires a non-empty name and validates slug format.
92
+ * @param {Object} tagData - The tag data to validate
93
+ * @param {string} tagData.name - Tag name (required, non-empty)
94
+ * @param {string} [tagData.slug] - Tag slug (lowercase letters, numbers, hyphens only)
95
+ * @throws {ValidationError} If validation fails
96
+ */
97
+ validateTagData(tagData) {
98
+ const errors = [];
99
+
100
+ if (!tagData.name || tagData.name.trim().length === 0) {
101
+ errors.push({ field: 'name', message: 'Tag name is required' });
102
+ }
103
+
104
+ if (tagData.slug && !/^[a-z0-9-]+$/.test(tagData.slug)) {
105
+ errors.push({
106
+ field: 'slug',
107
+ message: 'Slug must contain only lowercase letters, numbers, and hyphens',
108
+ });
109
+ }
110
+
111
+ if (errors.length > 0) {
112
+ throw new ValidationError('Tag validation failed', errors);
113
+ }
114
+ },
115
+
116
+ /**
117
+ * Validates tag update data. Name is optional but cannot be empty if provided.
118
+ * Validates slug format if provided.
119
+ * @param {Object} updateData - The tag update data to validate
120
+ * @param {string} [updateData.name] - Tag name (cannot be empty if provided)
121
+ * @param {string} [updateData.slug] - Tag slug (lowercase letters, numbers, hyphens only)
122
+ * @throws {ValidationError} If validation fails
123
+ */
124
+ validateTagUpdateData(updateData) {
125
+ const errors = [];
126
+
127
+ // Name is optional in updates, but if provided, it cannot be empty
128
+ if (updateData.name !== undefined && updateData.name.trim().length === 0) {
129
+ errors.push({ field: 'name', message: 'Tag name cannot be empty' });
130
+ }
131
+
132
+ // Validate slug format if provided
133
+ if (updateData.slug && !/^[a-z0-9-]+$/.test(updateData.slug)) {
134
+ errors.push({
135
+ field: 'slug',
136
+ message: 'Slug must contain only lowercase letters, numbers, and hyphens',
137
+ });
138
+ }
139
+
140
+ if (errors.length > 0) {
141
+ throw new ValidationError('Tag update validation failed', errors);
142
+ }
143
+ },
144
+
145
+ /**
146
+ * Validates that an image path is a non-empty string and that the file exists on disk.
147
+ * @param {string} imagePath - Absolute path to the image file
148
+ * @returns {Promise<void>}
149
+ * @throws {ValidationError} If imagePath is falsy or not a string
150
+ * @throws {NotFoundError} If the file does not exist at the given path
151
+ */
152
+ async validateImagePath(imagePath) {
153
+ if (!imagePath || typeof imagePath !== 'string') {
154
+ throw new ValidationError('Image path is required and must be a string');
155
+ }
156
+
157
+ // Check if file exists
158
+ try {
159
+ await fs.access(imagePath);
160
+ } catch {
161
+ throw new NotFoundError('Image file', imagePath);
162
+ }
163
+ },
164
+
165
+ /**
166
+ * Validates page creation data. Requires a title and either html or mobiledoc content.
167
+ * Also validates status values and delegates to validateScheduledStatus for scheduling.
168
+ * @param {Object} pageData - The page data to validate
169
+ * @param {string} pageData.title - Page title (required, non-empty)
170
+ * @param {string} [pageData.html] - HTML content (required if mobiledoc not provided)
171
+ * @param {string} [pageData.mobiledoc] - Mobiledoc content (required if html not provided)
172
+ * @param {string} [pageData.status] - Page status ('draft', 'published', 'scheduled')
173
+ * @param {string} [pageData.published_at] - ISO 8601 date for scheduled pages
174
+ * @throws {ValidationError} If validation fails
175
+ */
176
+ validatePageData(pageData) {
177
+ const errors = [];
178
+
179
+ if (!pageData.title || pageData.title.trim().length === 0) {
180
+ errors.push({ field: 'title', message: 'Title is required' });
181
+ }
182
+
183
+ if (!pageData.html && !pageData.mobiledoc) {
184
+ errors.push({ field: 'content', message: 'Either html or mobiledoc content is required' });
185
+ }
186
+
187
+ if (pageData.status && !['draft', 'published', 'scheduled'].includes(pageData.status)) {
188
+ errors.push({
189
+ field: 'status',
190
+ message: 'Invalid status. Must be draft, published, or scheduled',
191
+ });
192
+ }
193
+
194
+ if (errors.length > 0) {
195
+ throw new ValidationError('Page validation failed', errors);
196
+ }
197
+
198
+ this.validateScheduledStatus(pageData, 'Page');
199
+ },
200
+
201
+ /**
202
+ * Validates newsletter creation data. Requires a non-empty name.
203
+ * @param {Object} newsletterData - The newsletter data to validate
204
+ * @param {string} newsletterData.name - Newsletter name (required, non-empty)
205
+ * @throws {ValidationError} If validation fails
206
+ */
207
+ validateNewsletterData(newsletterData) {
208
+ const errors = [];
209
+
210
+ if (!newsletterData.name || newsletterData.name.trim().length === 0) {
211
+ errors.push({ field: 'name', message: 'Newsletter name is required' });
212
+ }
213
+
214
+ if (errors.length > 0) {
215
+ throw new ValidationError('Newsletter validation failed', errors);
216
+ }
217
+ },
218
+ };