@jgardner04/ghost-mcp-server 1.7.0 → 1.9.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 +117 -0
- package/src/schemas/__tests__/common.test.js +275 -0
- package/src/schemas/__tests__/postSchemas.test.js +194 -0
- package/src/schemas/__tests__/tagSchemas.test.js +262 -0
- package/src/schemas/common.js +227 -0
- package/src/schemas/index.js +39 -0
- package/src/schemas/memberSchemas.js +188 -0
- package/src/schemas/newsletterSchemas.js +168 -0
- package/src/schemas/pageSchemas.js +215 -0
- package/src/schemas/postSchemas.js +210 -0
- package/src/schemas/tagSchemas.js +136 -0
- package/src/schemas/tierSchemas.js +206 -0
- package/src/services/__tests__/ghostServiceImproved.members.test.js +261 -0
- package/src/services/__tests__/memberService.test.js +245 -0
- package/src/services/ghostServiceImproved.js +107 -0
- package/src/services/memberService.js +202 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ghostIdSchema, emailSchema, slugSchema } from './common.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Newsletter Schemas for Ghost CMS
|
|
6
|
+
* Provides input/output validation for newsletter operations
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ----- Input Schemas -----
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Schema for creating a new newsletter
|
|
13
|
+
* Required: name
|
|
14
|
+
* Optional: description, sender details, design options, etc.
|
|
15
|
+
*/
|
|
16
|
+
export const createNewsletterSchema = z.object({
|
|
17
|
+
name: z.string().min(1, 'Name cannot be empty').max(191, 'Name cannot exceed 191 characters'),
|
|
18
|
+
description: z.string().max(2000, 'Description cannot exceed 2000 characters').optional(),
|
|
19
|
+
slug: slugSchema.optional(),
|
|
20
|
+
sender_name: z.string().max(191, 'Sender name cannot exceed 191 characters').optional(),
|
|
21
|
+
sender_email: emailSchema.optional(),
|
|
22
|
+
sender_reply_to: z
|
|
23
|
+
.enum(['newsletter', 'support'], {
|
|
24
|
+
errorMap: () => ({ message: 'Sender reply-to must be newsletter or support' }),
|
|
25
|
+
})
|
|
26
|
+
.default('newsletter'),
|
|
27
|
+
status: z
|
|
28
|
+
.enum(['active', 'archived'], {
|
|
29
|
+
errorMap: () => ({ message: 'Status must be active or archived' }),
|
|
30
|
+
})
|
|
31
|
+
.default('active'),
|
|
32
|
+
visibility: z
|
|
33
|
+
.enum(['members', 'paid'], {
|
|
34
|
+
errorMap: () => ({ message: 'Visibility must be members or paid' }),
|
|
35
|
+
})
|
|
36
|
+
.default('members'),
|
|
37
|
+
subscribe_on_signup: z
|
|
38
|
+
.boolean()
|
|
39
|
+
.default(true)
|
|
40
|
+
.describe('Whether new members are automatically subscribed'),
|
|
41
|
+
sort_order: z
|
|
42
|
+
.number()
|
|
43
|
+
.int()
|
|
44
|
+
.min(0, 'Sort order must be non-negative')
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('Display order for newsletters'),
|
|
47
|
+
header_image: z.string().url('Invalid header image URL').optional(),
|
|
48
|
+
show_header_icon: z.boolean().default(true),
|
|
49
|
+
show_header_title: z.boolean().default(true),
|
|
50
|
+
title_font_category: z
|
|
51
|
+
.enum(['serif', 'sans-serif'], {
|
|
52
|
+
errorMap: () => ({ message: 'Title font category must be serif or sans-serif' }),
|
|
53
|
+
})
|
|
54
|
+
.default('sans-serif'),
|
|
55
|
+
title_alignment: z
|
|
56
|
+
.enum(['left', 'center'], {
|
|
57
|
+
errorMap: () => ({ message: 'Title alignment must be left or center' }),
|
|
58
|
+
})
|
|
59
|
+
.default('center'),
|
|
60
|
+
show_feature_image: z.boolean().default(true),
|
|
61
|
+
body_font_category: z
|
|
62
|
+
.enum(['serif', 'sans-serif'], {
|
|
63
|
+
errorMap: () => ({ message: 'Body font category must be serif or sans-serif' }),
|
|
64
|
+
})
|
|
65
|
+
.default('sans-serif'),
|
|
66
|
+
footer_content: z.string().optional(),
|
|
67
|
+
show_badge: z.boolean().default(true),
|
|
68
|
+
show_header_name: z.boolean().default(true).optional(),
|
|
69
|
+
show_post_title_section: z.boolean().default(true).optional(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Schema for updating an existing newsletter
|
|
74
|
+
* All fields optional
|
|
75
|
+
*/
|
|
76
|
+
export const updateNewsletterSchema = createNewsletterSchema.partial();
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Schema for newsletter query/filter parameters
|
|
80
|
+
*/
|
|
81
|
+
export const newsletterQuerySchema = z.object({
|
|
82
|
+
limit: z.number().int().min(1).max(100).default(15).optional(),
|
|
83
|
+
page: z.number().int().min(1).default(1).optional(),
|
|
84
|
+
filter: z
|
|
85
|
+
.string()
|
|
86
|
+
.regex(/^[a-zA-Z0-9_\-:.'"\s,[\]<>=!+]+$/, 'Invalid filter: contains disallowed characters')
|
|
87
|
+
.optional()
|
|
88
|
+
.describe('NQL filter string (e.g., "status:active")'),
|
|
89
|
+
order: z
|
|
90
|
+
.string()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe('Order results (e.g., "sort_order ASC", "created_at DESC")'),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Schema for newsletter ID parameter
|
|
97
|
+
*/
|
|
98
|
+
export const newsletterIdSchema = z.object({
|
|
99
|
+
id: ghostIdSchema,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Schema for newsletter slug parameter
|
|
104
|
+
*/
|
|
105
|
+
export const newsletterSlugSchema = z.object({
|
|
106
|
+
slug: slugSchema,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ----- Output Schemas -----
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Schema for a Ghost newsletter object (as returned by the API)
|
|
113
|
+
*/
|
|
114
|
+
export const newsletterOutputSchema = z.object({
|
|
115
|
+
id: ghostIdSchema,
|
|
116
|
+
uuid: z.string().uuid(),
|
|
117
|
+
name: z.string(),
|
|
118
|
+
description: z.string().nullable().optional(),
|
|
119
|
+
slug: z.string(),
|
|
120
|
+
sender_name: z.string().nullable().optional(),
|
|
121
|
+
sender_email: z.string().email().nullable().optional(),
|
|
122
|
+
sender_reply_to: z.enum(['newsletter', 'support']),
|
|
123
|
+
status: z.enum(['active', 'archived']),
|
|
124
|
+
visibility: z.enum(['members', 'paid']),
|
|
125
|
+
subscribe_on_signup: z.boolean(),
|
|
126
|
+
sort_order: z.number(),
|
|
127
|
+
header_image: z.string().url().nullable().optional(),
|
|
128
|
+
show_header_icon: z.boolean(),
|
|
129
|
+
show_header_title: z.boolean(),
|
|
130
|
+
title_font_category: z.string(),
|
|
131
|
+
title_alignment: z.enum(['left', 'center']),
|
|
132
|
+
show_feature_image: z.boolean(),
|
|
133
|
+
body_font_category: z.string(),
|
|
134
|
+
footer_content: z.string().nullable().optional(),
|
|
135
|
+
show_badge: z.boolean(),
|
|
136
|
+
show_header_name: z.boolean().optional(),
|
|
137
|
+
show_post_title_section: z.boolean().optional(),
|
|
138
|
+
created_at: z.string().datetime(),
|
|
139
|
+
updated_at: z.string().datetime(),
|
|
140
|
+
count: z
|
|
141
|
+
.object({
|
|
142
|
+
members: z.number().optional(),
|
|
143
|
+
posts: z.number().optional(),
|
|
144
|
+
})
|
|
145
|
+
.optional(),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Schema for array of newsletters
|
|
150
|
+
*/
|
|
151
|
+
export const newslettersArraySchema = z.array(newsletterOutputSchema);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Schema for paginated newsletter response
|
|
155
|
+
*/
|
|
156
|
+
export const newslettersPaginatedSchema = z.object({
|
|
157
|
+
newsletters: newslettersArraySchema,
|
|
158
|
+
meta: z.object({
|
|
159
|
+
pagination: z.object({
|
|
160
|
+
page: z.number(),
|
|
161
|
+
limit: z.number(),
|
|
162
|
+
pages: z.number(),
|
|
163
|
+
total: z.number(),
|
|
164
|
+
next: z.number().nullable(),
|
|
165
|
+
prev: z.number().nullable(),
|
|
166
|
+
}),
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
ghostIdSchema,
|
|
4
|
+
slugSchema,
|
|
5
|
+
titleSchema,
|
|
6
|
+
htmlContentSchema,
|
|
7
|
+
excerptSchema,
|
|
8
|
+
metaTitleSchema,
|
|
9
|
+
metaDescriptionSchema,
|
|
10
|
+
canonicalUrlSchema,
|
|
11
|
+
featureImageSchema,
|
|
12
|
+
featureImageAltSchema,
|
|
13
|
+
featuredSchema,
|
|
14
|
+
postStatusSchema,
|
|
15
|
+
visibilitySchema,
|
|
16
|
+
authorsSchema,
|
|
17
|
+
tagsSchema,
|
|
18
|
+
customExcerptSchema,
|
|
19
|
+
ogImageSchema,
|
|
20
|
+
twitterImageSchema,
|
|
21
|
+
isoDateSchema,
|
|
22
|
+
} from './common.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Page Schemas for Ghost CMS
|
|
26
|
+
* Pages are similar to posts but represent static content (About, Contact, etc.)
|
|
27
|
+
* Provides input/output validation for page operations
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// ----- Input Schemas -----
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Schema for creating a new page
|
|
34
|
+
* Required: title, html
|
|
35
|
+
* Optional: various metadata, feature image, authors, tags, etc.
|
|
36
|
+
* Note: Pages typically don't have tags, but Ghost API supports them
|
|
37
|
+
*/
|
|
38
|
+
export const createPageSchema = z.object({
|
|
39
|
+
title: titleSchema,
|
|
40
|
+
html: htmlContentSchema.describe('HTML content of the page'),
|
|
41
|
+
slug: slugSchema.optional(),
|
|
42
|
+
status: postStatusSchema.default('draft'),
|
|
43
|
+
visibility: visibilitySchema.default('public'),
|
|
44
|
+
featured: featuredSchema,
|
|
45
|
+
feature_image: featureImageSchema,
|
|
46
|
+
feature_image_alt: featureImageAltSchema,
|
|
47
|
+
feature_image_caption: z.string().max(500, 'Caption cannot exceed 500 characters').optional(),
|
|
48
|
+
excerpt: excerptSchema,
|
|
49
|
+
custom_excerpt: customExcerptSchema,
|
|
50
|
+
meta_title: metaTitleSchema,
|
|
51
|
+
meta_description: metaDescriptionSchema,
|
|
52
|
+
og_image: ogImageSchema,
|
|
53
|
+
og_title: z.string().max(300, 'OG title cannot exceed 300 characters').optional(),
|
|
54
|
+
og_description: z.string().max(500, 'OG description cannot exceed 500 characters').optional(),
|
|
55
|
+
twitter_image: twitterImageSchema,
|
|
56
|
+
twitter_title: z.string().max(300, 'Twitter title cannot exceed 300 characters').optional(),
|
|
57
|
+
twitter_description: z
|
|
58
|
+
.string()
|
|
59
|
+
.max(500, 'Twitter description cannot exceed 500 characters')
|
|
60
|
+
.optional(),
|
|
61
|
+
canonical_url: canonicalUrlSchema,
|
|
62
|
+
tags: tagsSchema.describe('Array of tag names or IDs (rarely used for pages)'),
|
|
63
|
+
authors: authorsSchema.describe('Array of author IDs or emails'),
|
|
64
|
+
published_at: isoDateSchema.optional().describe('Scheduled publish time (ISO 8601 format)'),
|
|
65
|
+
updated_at: isoDateSchema.optional(),
|
|
66
|
+
created_at: isoDateSchema.optional(),
|
|
67
|
+
codeinjection_head: z.string().optional(),
|
|
68
|
+
codeinjection_foot: z.string().optional(),
|
|
69
|
+
custom_template: z.string().optional().describe('Custom template filename'),
|
|
70
|
+
show_title_and_feature_image: z
|
|
71
|
+
.boolean()
|
|
72
|
+
.default(true)
|
|
73
|
+
.describe('Whether to show title and feature image on page'),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Schema for updating an existing page
|
|
78
|
+
* All fields optional
|
|
79
|
+
*/
|
|
80
|
+
export const updatePageSchema = createPageSchema.partial();
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Schema for page query/filter parameters
|
|
84
|
+
*/
|
|
85
|
+
export const pageQuerySchema = z.object({
|
|
86
|
+
limit: z.number().int().min(1).max(100).default(15).optional(),
|
|
87
|
+
page: z.number().int().min(1).default(1).optional(),
|
|
88
|
+
filter: z
|
|
89
|
+
.string()
|
|
90
|
+
.regex(/^[a-zA-Z0-9_\-:.'"\s,[\]<>=!+]+$/, 'Invalid filter: contains disallowed characters')
|
|
91
|
+
.optional()
|
|
92
|
+
.describe('NQL filter string (e.g., "status:published+featured:true")'),
|
|
93
|
+
include: z
|
|
94
|
+
.string()
|
|
95
|
+
.optional()
|
|
96
|
+
.describe('Comma-separated list of relations (e.g., "tags,authors")'),
|
|
97
|
+
fields: z.string().optional().describe('Comma-separated list of fields to return'),
|
|
98
|
+
formats: z
|
|
99
|
+
.string()
|
|
100
|
+
.optional()
|
|
101
|
+
.describe('Comma-separated list of formats (html, plaintext, mobiledoc)'),
|
|
102
|
+
order: z.string().optional().describe('Order results (e.g., "published_at DESC", "title ASC")'),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Schema for page ID parameter
|
|
107
|
+
*/
|
|
108
|
+
export const pageIdSchema = z.object({
|
|
109
|
+
id: ghostIdSchema,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Schema for page slug parameter
|
|
114
|
+
*/
|
|
115
|
+
export const pageSlugSchema = z.object({
|
|
116
|
+
slug: slugSchema,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ----- Output Schemas -----
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Schema for an author object (nested in page)
|
|
123
|
+
* Reusing from postSchemas would be better, but duplicated here for independence
|
|
124
|
+
*/
|
|
125
|
+
export const authorOutputSchema = z.object({
|
|
126
|
+
id: ghostIdSchema,
|
|
127
|
+
name: z.string(),
|
|
128
|
+
slug: z.string(),
|
|
129
|
+
email: z.string().email().optional(),
|
|
130
|
+
profile_image: z.string().url().nullable().optional(),
|
|
131
|
+
cover_image: z.string().url().nullable().optional(),
|
|
132
|
+
bio: z.string().nullable().optional(),
|
|
133
|
+
website: z.string().url().nullable().optional(),
|
|
134
|
+
location: z.string().nullable().optional(),
|
|
135
|
+
facebook: z.string().nullable().optional(),
|
|
136
|
+
twitter: z.string().nullable().optional(),
|
|
137
|
+
url: z.string().url(),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Schema for a tag object (nested in page)
|
|
142
|
+
*/
|
|
143
|
+
export const tagOutputSchema = z.object({
|
|
144
|
+
id: ghostIdSchema,
|
|
145
|
+
name: z.string(),
|
|
146
|
+
slug: z.string(),
|
|
147
|
+
description: z.string().nullable().optional(),
|
|
148
|
+
feature_image: z.string().url().nullable().optional(),
|
|
149
|
+
visibility: visibilitySchema,
|
|
150
|
+
url: z.string().url(),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Schema for a Ghost page object (as returned by the API)
|
|
155
|
+
*/
|
|
156
|
+
export const pageOutputSchema = z.object({
|
|
157
|
+
id: ghostIdSchema,
|
|
158
|
+
uuid: z.string().uuid(),
|
|
159
|
+
title: z.string(),
|
|
160
|
+
slug: z.string(),
|
|
161
|
+
html: z.string().nullable().optional(),
|
|
162
|
+
comment_id: z.string().nullable().optional(),
|
|
163
|
+
feature_image: z.string().url().nullable().optional(),
|
|
164
|
+
feature_image_alt: z.string().nullable().optional(),
|
|
165
|
+
feature_image_caption: z.string().nullable().optional(),
|
|
166
|
+
featured: z.boolean(),
|
|
167
|
+
status: postStatusSchema,
|
|
168
|
+
visibility: visibilitySchema,
|
|
169
|
+
created_at: z.string().datetime(),
|
|
170
|
+
updated_at: z.string().datetime(),
|
|
171
|
+
published_at: z.string().datetime().nullable().optional(),
|
|
172
|
+
custom_excerpt: z.string().nullable().optional(),
|
|
173
|
+
codeinjection_head: z.string().nullable().optional(),
|
|
174
|
+
codeinjection_foot: z.string().nullable().optional(),
|
|
175
|
+
custom_template: z.string().nullable().optional(),
|
|
176
|
+
canonical_url: z.string().url().nullable().optional(),
|
|
177
|
+
url: z.string().url(),
|
|
178
|
+
excerpt: z.string().nullable().optional(),
|
|
179
|
+
reading_time: z.number().nullable().optional(),
|
|
180
|
+
og_image: z.string().url().nullable().optional(),
|
|
181
|
+
og_title: z.string().nullable().optional(),
|
|
182
|
+
og_description: z.string().nullable().optional(),
|
|
183
|
+
twitter_image: z.string().url().nullable().optional(),
|
|
184
|
+
twitter_title: z.string().nullable().optional(),
|
|
185
|
+
twitter_description: z.string().nullable().optional(),
|
|
186
|
+
meta_title: z.string().nullable().optional(),
|
|
187
|
+
meta_description: z.string().nullable().optional(),
|
|
188
|
+
show_title_and_feature_image: z.boolean().optional(),
|
|
189
|
+
authors: z.array(authorOutputSchema).optional(),
|
|
190
|
+
tags: z.array(tagOutputSchema).optional(),
|
|
191
|
+
primary_author: authorOutputSchema.nullable().optional(),
|
|
192
|
+
primary_tag: tagOutputSchema.nullable().optional(),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Schema for array of pages
|
|
197
|
+
*/
|
|
198
|
+
export const pagesArraySchema = z.array(pageOutputSchema);
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Schema for paginated page response
|
|
202
|
+
*/
|
|
203
|
+
export const pagesPaginatedSchema = z.object({
|
|
204
|
+
pages: pagesArraySchema,
|
|
205
|
+
meta: z.object({
|
|
206
|
+
pagination: z.object({
|
|
207
|
+
page: z.number(),
|
|
208
|
+
limit: z.number(),
|
|
209
|
+
pages: z.number(),
|
|
210
|
+
total: z.number(),
|
|
211
|
+
next: z.number().nullable(),
|
|
212
|
+
prev: z.number().nullable(),
|
|
213
|
+
}),
|
|
214
|
+
}),
|
|
215
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
ghostIdSchema,
|
|
4
|
+
slugSchema,
|
|
5
|
+
titleSchema,
|
|
6
|
+
htmlContentSchema,
|
|
7
|
+
excerptSchema,
|
|
8
|
+
metaTitleSchema,
|
|
9
|
+
metaDescriptionSchema,
|
|
10
|
+
canonicalUrlSchema,
|
|
11
|
+
featureImageSchema,
|
|
12
|
+
featureImageAltSchema,
|
|
13
|
+
featuredSchema,
|
|
14
|
+
postStatusSchema,
|
|
15
|
+
visibilitySchema,
|
|
16
|
+
authorsSchema,
|
|
17
|
+
tagsSchema,
|
|
18
|
+
customExcerptSchema,
|
|
19
|
+
ogImageSchema,
|
|
20
|
+
twitterImageSchema,
|
|
21
|
+
isoDateSchema,
|
|
22
|
+
} from './common.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Post Schemas for Ghost CMS
|
|
26
|
+
* Provides input/output validation for post operations
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
// ----- Input Schemas -----
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Schema for creating a new post
|
|
33
|
+
* Required: title, html
|
|
34
|
+
* Optional: various metadata, feature image, authors, tags, etc.
|
|
35
|
+
*/
|
|
36
|
+
export const createPostSchema = z.object({
|
|
37
|
+
title: titleSchema,
|
|
38
|
+
html: htmlContentSchema.describe('HTML content of the post'),
|
|
39
|
+
slug: slugSchema.optional(),
|
|
40
|
+
status: postStatusSchema.default('draft'),
|
|
41
|
+
visibility: visibilitySchema.default('public'),
|
|
42
|
+
featured: featuredSchema,
|
|
43
|
+
feature_image: featureImageSchema,
|
|
44
|
+
feature_image_alt: featureImageAltSchema,
|
|
45
|
+
feature_image_caption: z.string().max(500, 'Caption cannot exceed 500 characters').optional(),
|
|
46
|
+
excerpt: excerptSchema,
|
|
47
|
+
custom_excerpt: customExcerptSchema,
|
|
48
|
+
meta_title: metaTitleSchema,
|
|
49
|
+
meta_description: metaDescriptionSchema,
|
|
50
|
+
og_image: ogImageSchema,
|
|
51
|
+
og_title: z.string().max(300, 'OG title cannot exceed 300 characters').optional(),
|
|
52
|
+
og_description: z.string().max(500, 'OG description cannot exceed 500 characters').optional(),
|
|
53
|
+
twitter_image: twitterImageSchema,
|
|
54
|
+
twitter_title: z.string().max(300, 'Twitter title cannot exceed 300 characters').optional(),
|
|
55
|
+
twitter_description: z
|
|
56
|
+
.string()
|
|
57
|
+
.max(500, 'Twitter description cannot exceed 500 characters')
|
|
58
|
+
.optional(),
|
|
59
|
+
canonical_url: canonicalUrlSchema,
|
|
60
|
+
tags: tagsSchema.describe('Array of tag names or IDs to associate with the post'),
|
|
61
|
+
authors: authorsSchema.describe('Array of author IDs or emails'),
|
|
62
|
+
published_at: isoDateSchema.optional().describe('Scheduled publish time (ISO 8601 format)'),
|
|
63
|
+
updated_at: isoDateSchema.optional(),
|
|
64
|
+
created_at: isoDateSchema.optional(),
|
|
65
|
+
codeinjection_head: z.string().optional(),
|
|
66
|
+
codeinjection_foot: z.string().optional(),
|
|
67
|
+
custom_template: z.string().optional().describe('Custom template filename'),
|
|
68
|
+
email_only: z.boolean().default(false).describe('Whether post is email-only'),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Schema for updating an existing post
|
|
73
|
+
* All fields optional
|
|
74
|
+
*/
|
|
75
|
+
export const updatePostSchema = createPostSchema.partial();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Schema for post query/filter parameters
|
|
79
|
+
*/
|
|
80
|
+
export const postQuerySchema = z.object({
|
|
81
|
+
limit: z.number().int().min(1).max(100).default(15).optional(),
|
|
82
|
+
page: z.number().int().min(1).default(1).optional(),
|
|
83
|
+
filter: z
|
|
84
|
+
.string()
|
|
85
|
+
.regex(/^[a-zA-Z0-9_\-:.'"\s,[\]<>=!+]+$/, 'Invalid filter: contains disallowed characters')
|
|
86
|
+
.optional()
|
|
87
|
+
.describe('NQL filter string (e.g., "status:published+featured:true")'),
|
|
88
|
+
include: z
|
|
89
|
+
.string()
|
|
90
|
+
.optional()
|
|
91
|
+
.describe('Comma-separated list of relations (e.g., "tags,authors")'),
|
|
92
|
+
fields: z.string().optional().describe('Comma-separated list of fields to return'),
|
|
93
|
+
formats: z
|
|
94
|
+
.string()
|
|
95
|
+
.optional()
|
|
96
|
+
.describe('Comma-separated list of formats (html, plaintext, mobiledoc)'),
|
|
97
|
+
order: z.string().optional().describe('Order results (e.g., "published_at DESC", "title ASC")'),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Schema for post ID parameter
|
|
102
|
+
*/
|
|
103
|
+
export const postIdSchema = z.object({
|
|
104
|
+
id: ghostIdSchema,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Schema for post slug parameter
|
|
109
|
+
*/
|
|
110
|
+
export const postSlugSchema = z.object({
|
|
111
|
+
slug: slugSchema,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ----- Output Schemas -----
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Schema for an author object (nested in post)
|
|
118
|
+
*/
|
|
119
|
+
export const authorOutputSchema = z.object({
|
|
120
|
+
id: ghostIdSchema,
|
|
121
|
+
name: z.string(),
|
|
122
|
+
slug: z.string(),
|
|
123
|
+
email: z.string().email().optional(),
|
|
124
|
+
profile_image: z.string().url().nullable().optional(),
|
|
125
|
+
cover_image: z.string().url().nullable().optional(),
|
|
126
|
+
bio: z.string().nullable().optional(),
|
|
127
|
+
website: z.string().url().nullable().optional(),
|
|
128
|
+
location: z.string().nullable().optional(),
|
|
129
|
+
facebook: z.string().nullable().optional(),
|
|
130
|
+
twitter: z.string().nullable().optional(),
|
|
131
|
+
url: z.string().url(),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Schema for a tag object (nested in post)
|
|
136
|
+
*/
|
|
137
|
+
export const tagOutputSchema = z.object({
|
|
138
|
+
id: ghostIdSchema,
|
|
139
|
+
name: z.string(),
|
|
140
|
+
slug: z.string(),
|
|
141
|
+
description: z.string().nullable().optional(),
|
|
142
|
+
feature_image: z.string().url().nullable().optional(),
|
|
143
|
+
visibility: visibilitySchema,
|
|
144
|
+
url: z.string().url(),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Schema for a Ghost post object (as returned by the API)
|
|
149
|
+
*/
|
|
150
|
+
export const postOutputSchema = z.object({
|
|
151
|
+
id: ghostIdSchema,
|
|
152
|
+
uuid: z.string().uuid(),
|
|
153
|
+
title: z.string(),
|
|
154
|
+
slug: z.string(),
|
|
155
|
+
html: z.string().nullable().optional(),
|
|
156
|
+
comment_id: z.string().nullable().optional(),
|
|
157
|
+
feature_image: z.string().url().nullable().optional(),
|
|
158
|
+
feature_image_alt: z.string().nullable().optional(),
|
|
159
|
+
feature_image_caption: z.string().nullable().optional(),
|
|
160
|
+
featured: z.boolean(),
|
|
161
|
+
status: postStatusSchema,
|
|
162
|
+
visibility: visibilitySchema,
|
|
163
|
+
created_at: z.string().datetime(),
|
|
164
|
+
updated_at: z.string().datetime(),
|
|
165
|
+
published_at: z.string().datetime().nullable().optional(),
|
|
166
|
+
custom_excerpt: z.string().nullable().optional(),
|
|
167
|
+
codeinjection_head: z.string().nullable().optional(),
|
|
168
|
+
codeinjection_foot: z.string().nullable().optional(),
|
|
169
|
+
custom_template: z.string().nullable().optional(),
|
|
170
|
+
canonical_url: z.string().url().nullable().optional(),
|
|
171
|
+
url: z.string().url(),
|
|
172
|
+
excerpt: z.string().nullable().optional(),
|
|
173
|
+
reading_time: z.number().nullable().optional(),
|
|
174
|
+
email_only: z.boolean().optional(),
|
|
175
|
+
og_image: z.string().url().nullable().optional(),
|
|
176
|
+
og_title: z.string().nullable().optional(),
|
|
177
|
+
og_description: z.string().nullable().optional(),
|
|
178
|
+
twitter_image: z.string().url().nullable().optional(),
|
|
179
|
+
twitter_title: z.string().nullable().optional(),
|
|
180
|
+
twitter_description: z.string().nullable().optional(),
|
|
181
|
+
meta_title: z.string().nullable().optional(),
|
|
182
|
+
meta_description: z.string().nullable().optional(),
|
|
183
|
+
email_subject: z.string().nullable().optional(),
|
|
184
|
+
authors: z.array(authorOutputSchema).optional(),
|
|
185
|
+
tags: z.array(tagOutputSchema).optional(),
|
|
186
|
+
primary_author: authorOutputSchema.nullable().optional(),
|
|
187
|
+
primary_tag: tagOutputSchema.nullable().optional(),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Schema for array of posts
|
|
192
|
+
*/
|
|
193
|
+
export const postsArraySchema = z.array(postOutputSchema);
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Schema for paginated post response
|
|
197
|
+
*/
|
|
198
|
+
export const postsPaginatedSchema = z.object({
|
|
199
|
+
posts: postsArraySchema,
|
|
200
|
+
meta: z.object({
|
|
201
|
+
pagination: z.object({
|
|
202
|
+
page: z.number(),
|
|
203
|
+
limit: z.number(),
|
|
204
|
+
pages: z.number(),
|
|
205
|
+
total: z.number(),
|
|
206
|
+
next: z.number().nullable(),
|
|
207
|
+
prev: z.number().nullable(),
|
|
208
|
+
}),
|
|
209
|
+
}),
|
|
210
|
+
});
|