Skip to content

Commit 4c4cecf

Browse files
committed
fix: do not trigger site.changed when draft pages are deleted
Applies the draft-aware cache invalidation guard to pages and lightens the status lookups to fetch only the status column. Adds e2e-webhooks coverage for the bulk-posts path, draft pages (single + bulk), and guards that published post/page deletion still invalidates the cache.
1 parent 50908be commit 4c4cecf

3 files changed

Lines changed: 194 additions & 10 deletions

File tree

ghost/core/core/server/api/endpoints/pages.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,19 @@ const controller = {
221221
method: 'destroy'
222222
},
223223
async query(frame) {
224+
const pagesToDelete = await models.Post.findAll({
225+
filter: frame.options.filter,
226+
status: 'all',
227+
columns: ['status']
228+
});
229+
230+
const allDraft = pagesToDelete.length > 0 && pagesToDelete.every((page) => {
231+
return page.get('status') === 'draft';
232+
});
233+
if (allDraft) {
234+
frame.setHeader('X-Cache-Invalidate', '');
235+
}
236+
224237
return await postsService.bulkDestroy(frame.options);
225238
}
226239
},
@@ -248,7 +261,17 @@ const controller = {
248261
docName: 'posts',
249262
unsafeAttrs: UNSAFE_ATTRS
250263
},
251-
query(frame) {
264+
async query(frame) {
265+
const page = await models.Post.findOne({
266+
id: frame.options.id,
267+
type: 'page',
268+
status: 'all'
269+
}, {require: false, columns: ['status']});
270+
271+
if (page && page.get('status') === 'draft') {
272+
frame.setHeader('X-Cache-Invalidate', '');
273+
}
274+
252275
return models.Post.destroy({...frame.options, require: true});
253276
}
254277
},

ghost/core/core/server/api/endpoints/posts.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ const controller = {
269269
bulkDestroy: {
270270
statusCode: 200,
271271
headers: {
272-
cacheInvalidate: false
272+
cacheInvalidate: true
273273
},
274274
options: [
275275
'filter'
@@ -280,14 +280,15 @@ const controller = {
280280
async query(frame) {
281281
const postsToDelete = await models.Post.findAll({
282282
filter: frame.options.filter,
283-
status: 'all'
283+
status: 'all',
284+
columns: ['status']
284285
});
285286

286-
const allDraft = postsToDelete.every((post) => {
287+
const allDraft = postsToDelete.length > 0 && postsToDelete.every((post) => {
287288
return post.get('status') === 'draft';
288289
});
289-
if (!allDraft) {
290-
frame.setHeader('X-Cache-Invalidate', '/*');
290+
if (allDraft) {
291+
frame.setHeader('X-Cache-Invalidate', '');
291292
}
292293

293294
return await postsService.bulkDestroy(frame.options);
@@ -297,7 +298,7 @@ const controller = {
297298
destroy: {
298299
statusCode: 204,
299300
headers: {
300-
cacheInvalidate: false
301+
cacheInvalidate: true
301302
},
302303
options: [
303304
'include',
@@ -320,10 +321,10 @@ const controller = {
320321
const post = await models.Post.findOne({
321322
id: frame.options.id,
322323
status: 'all'
323-
}, {require: false});
324+
}, {require: false, columns: ['status']});
324325

325-
if (post && post.get('status') !== 'draft') {
326-
frame.setHeader('X-Cache-Invalidate', '/*');
326+
if (post && post.get('status') === 'draft') {
327+
frame.setHeader('X-Cache-Invalidate', '');
327328
}
328329

329330
return models.Post.destroy({...frame.options, require: true});

ghost/core/test/e2e-webhooks/site.test.js

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,164 @@ describe('site.* events', function () {
163163
throw new Error('The webhook should not have been sent.');
164164
}
165165
});
166+
167+
it('site.changed event is NOT triggered when only draft posts are bulk deleted', async function () {
168+
const webhookURL = 'https://test-webhook-receiver.com/site-changed';
169+
await webhookMockReceiver.mock(webhookURL);
170+
await fixtureManager.insertWebhook({
171+
event: 'site.changed',
172+
url: webhookURL
173+
});
174+
175+
await adminAPIAgent
176+
.post('posts/')
177+
.body({
178+
posts: [{
179+
title: 'bulk draft webhookz',
180+
status: 'draft',
181+
mobiledoc: fixtureManager.get('posts', 1).mobiledoc
182+
}]
183+
})
184+
.expectStatus(201);
185+
186+
const filter = 'title:\'bulk draft webhookz\'';
187+
188+
await adminAPIAgent
189+
.delete('posts/?filter=' + encodeURIComponent(filter))
190+
.expectStatus(200);
191+
192+
const receivedRequest = webhookMockReceiver.receivedRequest().then(() => true);
193+
const wait = new Promise((resolve) => {
194+
setTimeout(resolve, 2000, false);
195+
});
196+
197+
const requestWasReceived = await Promise.race([
198+
receivedRequest,
199+
wait
200+
]);
201+
202+
if (requestWasReceived) {
203+
throw new Error('The webhook should not have been sent.');
204+
}
205+
});
206+
207+
it('invalidates the cache when a published post is deleted', async function () {
208+
const res = await adminAPIAgent
209+
.post('posts/')
210+
.body({
211+
posts: [{
212+
title: 'published webhookz',
213+
status: 'published',
214+
mobiledoc: fixtureManager.get('posts', 1).mobiledoc
215+
}]
216+
})
217+
.expectStatus(201);
218+
219+
const id = res.body.posts[0].id;
220+
221+
await adminAPIAgent
222+
.delete('posts/' + id)
223+
.expectStatus(204)
224+
.expectHeader('X-Cache-Invalidate', '/*');
225+
});
226+
227+
it('site.changed event is NOT triggered when a draft page is deleted', async function () {
228+
const webhookURL = 'https://test-webhook-receiver.com/site-changed';
229+
await webhookMockReceiver.mock(webhookURL);
230+
await fixtureManager.insertWebhook({
231+
event: 'site.changed',
232+
url: webhookURL
233+
});
234+
235+
const res = await adminAPIAgent
236+
.post('pages/')
237+
.body({
238+
pages: [{
239+
title: 'draft page webhookz',
240+
status: 'draft',
241+
mobiledoc: fixtureManager.get('posts', 1).mobiledoc
242+
}]
243+
})
244+
.expectStatus(201);
245+
246+
const id = res.body.pages[0].id;
247+
248+
await adminAPIAgent
249+
.delete('pages/' + id)
250+
.expectStatus(204);
251+
252+
const receivedRequest = webhookMockReceiver.receivedRequest().then(() => true);
253+
const wait = new Promise((resolve) => {
254+
setTimeout(resolve, 2000, false);
255+
});
256+
257+
const requestWasReceived = await Promise.race([
258+
receivedRequest,
259+
wait
260+
]);
261+
262+
if (requestWasReceived) {
263+
throw new Error('The webhook should not have been sent.');
264+
}
265+
});
266+
267+
it('site.changed event is NOT triggered when only draft pages are bulk deleted', async function () {
268+
const webhookURL = 'https://test-webhook-receiver.com/site-changed';
269+
await webhookMockReceiver.mock(webhookURL);
270+
await fixtureManager.insertWebhook({
271+
event: 'site.changed',
272+
url: webhookURL
273+
});
274+
275+
await adminAPIAgent
276+
.post('pages/')
277+
.body({
278+
pages: [{
279+
title: 'bulk draft page webhookz',
280+
status: 'draft',
281+
mobiledoc: fixtureManager.get('posts', 1).mobiledoc
282+
}]
283+
})
284+
.expectStatus(201);
285+
286+
const filter = 'title:\'bulk draft page webhookz\'';
287+
288+
await adminAPIAgent
289+
.delete('pages/?filter=' + encodeURIComponent(filter))
290+
.expectStatus(200);
291+
292+
const receivedRequest = webhookMockReceiver.receivedRequest().then(() => true);
293+
const wait = new Promise((resolve) => {
294+
setTimeout(resolve, 2000, false);
295+
});
296+
297+
const requestWasReceived = await Promise.race([
298+
receivedRequest,
299+
wait
300+
]);
301+
302+
if (requestWasReceived) {
303+
throw new Error('The webhook should not have been sent.');
304+
}
305+
});
306+
307+
it('invalidates the cache when a published page is deleted', async function () {
308+
const res = await adminAPIAgent
309+
.post('pages/')
310+
.body({
311+
pages: [{
312+
title: 'published page webhookz',
313+
status: 'published',
314+
mobiledoc: fixtureManager.get('posts', 1).mobiledoc
315+
}]
316+
})
317+
.expectStatus(201);
318+
319+
const id = res.body.pages[0].id;
320+
321+
await adminAPIAgent
322+
.delete('pages/' + id)
323+
.expectStatus(204)
324+
.expectHeader('X-Cache-Invalidate', '/*');
325+
});
166326
});

0 commit comments

Comments
 (0)