From 5dd6159ac680db0eb0b751451ca441f444dee400 Mon Sep 17 00:00:00 2001 From: Naz Date: Mon, 26 Jun 2023 16:29:32 +0700 Subject: [PATCH] Added handling for 'post.edited' Ghost model event refs https://github.com/TryGhost/Team/issues/3169 - Adds optomized collection update handling for when post.edited model event is emitted. --- ghost/collections/package.json | 3 +- ghost/collections/src/CollectionsService.ts | 34 +++++++++++- .../collections/src/events/PostEditedEvent.ts | 31 +++++++++++ ghost/collections/src/index.ts | 1 + ghost/collections/src/libraries.d.ts | 3 +- ghost/collections/test/Collection.test.ts | 3 +- ghost/collections/test/collections.test.ts | 54 ++++++++++++++++++- .../model-to-domain-events-bridge.js | 18 ++++++- 8 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 ghost/collections/src/events/PostEditedEvent.ts diff --git a/ghost/collections/package.json b/ghost/collections/package.json index e043608f80..a5cc71e9d9 100644 --- a/ghost/collections/package.json +++ b/ghost/collections/package.json @@ -21,8 +21,8 @@ "build" ], "devDependencies": { - "c8": "7.13.0", "@tryghost/domain-events": "0.0.0", + "c8": "7.13.0", "mocha": "10.2.0", "sinon": "15.0.4", "ts-node": "10.9.1", @@ -31,6 +31,7 @@ "dependencies": { "@tryghost/errors": "^1.2.25", "@tryghost/in-memory-repository": "0.0.0", + "@tryghost/logging": "^2.4.5", "@tryghost/nql": "^0.11.0", "@tryghost/tpl": "^0.1.25", "bson-objectid": "^2.0.4" diff --git a/ghost/collections/src/CollectionsService.ts b/ghost/collections/src/CollectionsService.ts index 1a044a93a6..e5bdc3076a 100644 --- a/ghost/collections/src/CollectionsService.ts +++ b/ghost/collections/src/CollectionsService.ts @@ -1,10 +1,12 @@ +import logging from '@tryghost/logging'; +import tpl from '@tryghost/tpl'; import {Collection} from './Collection'; import {CollectionResourceChangeEvent} from './CollectionResourceChangeEvent'; import {CollectionRepository} from './CollectionRepository'; -import tpl from '@tryghost/tpl'; import {MethodNotAllowedError, NotFoundError} from '@tryghost/errors'; import {PostDeletedEvent} from './events/PostDeletedEvent'; import {PostAddedEvent} from './events/PostAddedEvent'; +import {PostEditedEvent} from './events/PostEditedEvent'; const messages = { cannotDeleteBuiltInCollectionError: { @@ -153,6 +155,10 @@ export class CollectionsService { this.DomainEvents.subscribe(PostAddedEvent, async (event: PostAddedEvent) => { await this.addPostToMatchingCollections(event.data); }); + + this.DomainEvents.subscribe(PostEditedEvent, async (event: PostEditedEvent) => { + await this.updatePostInMatchingCollections(event.data); + }); } async createCollection(data: CollectionInputDTO): Promise { @@ -228,7 +234,6 @@ export class CollectionsService { }); for (const collection of collections) { - await collection.addPost(post); const added = await collection.addPost(post); if (added) { @@ -252,6 +257,31 @@ export class CollectionsService { } } + async updatePostInMatchingCollections(postEdit: PostEditedEvent['data']) { + const collections = await this.collectionsRepository.getAll({ + filter: 'type:automatic' + }); + + for (const collection of collections) { + if (collection.includesPost(postEdit.id) && !collection.postMatchesFilter(postEdit.current)) { + await collection.removePost(postEdit.id); + await this.collectionsRepository.save(collection); + + logging.info(`[Collections] Post ${postEdit.id} was updated and removed from collection ${collection.id} with filter ${collection.filter}`); + } else if (!collection.includesPost(postEdit.id) && collection.postMatchesFilter(postEdit.current)) { + const added = await collection.addPost(postEdit.current); + + if (added) { + await this.collectionsRepository.save(collection); + } + + logging.info(`[Collections] Post ${postEdit.id} was updated and added to collection ${collection.id} with filter ${collection.filter}`); + } else { + logging.info(`[Collections] Post ${postEdit.id} was updated but did not update any collections`); + } + } + } + async edit(data: any): Promise { const collection = await this.collectionsRepository.getById(data.id); diff --git a/ghost/collections/src/events/PostEditedEvent.ts b/ghost/collections/src/events/PostEditedEvent.ts new file mode 100644 index 0000000000..99089bde5e --- /dev/null +++ b/ghost/collections/src/events/PostEditedEvent.ts @@ -0,0 +1,31 @@ +type PostEditData = { + id: string; + current: { + id: string; + title: string; + featured: boolean; + published_at: Date; + }, + previous: { + id: string; + title: string; + featured: boolean; + published_at: Date; + } +}; + +export class PostEditedEvent { + id: string; + data: PostEditData; + timestamp: Date; + + constructor(data: any, timestamp: Date) { + this.id = data.id; + this.data = data; + this.timestamp = timestamp; + } + + static create(data: any, timestamp = new Date()) { + return new PostEditedEvent(data, timestamp); + } +} diff --git a/ghost/collections/src/index.ts b/ghost/collections/src/index.ts index ec4b31be9e..415d139276 100644 --- a/ghost/collections/src/index.ts +++ b/ghost/collections/src/index.ts @@ -4,3 +4,4 @@ export * from './Collection'; export * from './CollectionResourceChangeEvent'; export * from './events/PostDeletedEvent'; export * from './events/PostAddedEvent'; +export * from './events/PostEditedEvent'; diff --git a/ghost/collections/src/libraries.d.ts b/ghost/collections/src/libraries.d.ts index 80d4385009..f89ee4168b 100644 --- a/ghost/collections/src/libraries.d.ts +++ b/ghost/collections/src/libraries.d.ts @@ -1,4 +1,5 @@ declare module '@tryghost/errors'; -declare module '@tryghost/tpl'; declare module '@tryghost/domain-events' +declare module '@tryghost/logging' declare module '@tryghost/nql' +declare module '@tryghost/tpl'; diff --git a/ghost/collections/test/Collection.test.ts b/ghost/collections/test/Collection.test.ts index e87f653556..540eb7dc49 100644 --- a/ghost/collections/test/Collection.test.ts +++ b/ghost/collections/test/Collection.test.ts @@ -266,7 +266,8 @@ describe('Collection', function () { it('Can match a post with a filter', async function () { const collection = await Collection.create({ title: 'Testing filtering posts', - type: 'automatic' + type: 'automatic', + filter: 'featured:true' }); const featuredPost = { diff --git a/ghost/collections/test/collections.test.ts b/ghost/collections/test/collections.test.ts index a6983de2c1..a3a8bac839 100644 --- a/ghost/collections/test/collections.test.ts +++ b/ghost/collections/test/collections.test.ts @@ -6,7 +6,8 @@ import { CollectionsRepositoryInMemory, CollectionResourceChangeEvent, PostDeletedEvent, - PostAddedEvent + PostAddedEvent, + PostEditedEvent } from '../src/index'; import {PostsRepositoryInMemory} from './fixtures/PostsRepositoryInMemory'; import {posts} from './fixtures/posts'; @@ -417,6 +418,57 @@ describe('CollectionsService', function () { assert.equal((await collectionsService.getById(automaticNonFeaturedCollection.id))?.posts.length, 2); assert.equal((await collectionsService.getById(manualCollection.id))?.posts.length, 2); }); + + it('Moves post form featured to non featured collection when the featured attribute is changed', async function () { + collectionsService.subscribeToEvents(); + const newFeaturedPost = { + id: 'post-featured', + title: 'Post Featured', + slug: 'post-featured', + featured: false, + published_at: new Date('2023-03-16T07:19:07.447Z'), + deleted: false + }; + await postsRepository.save(newFeaturedPost); + const updateCollectionEvent = PostEditedEvent.create({ + id: newFeaturedPost.id, + current: { + id: newFeaturedPost.id, + featured: false + }, + previous: { + id: newFeaturedPost.id, + featured: true + } + }); + + DomainEvents.dispatch(updateCollectionEvent); + await DomainEvents.allSettled(); + + assert.equal((await collectionsService.getById(automaticFeaturedCollection.id))?.posts?.length, 2); + assert.equal((await collectionsService.getById(automaticNonFeaturedCollection.id))?.posts.length, 3); + assert.equal((await collectionsService.getById(manualCollection.id))?.posts.length, 2); + + // change featured back to true + const updateCollectionEventBackToFeatured = PostEditedEvent.create({ + id: newFeaturedPost.id, + current: { + id: newFeaturedPost.id, + featured: true + }, + previous: { + id: newFeaturedPost.id, + featured: false + } + }); + + DomainEvents.dispatch(updateCollectionEventBackToFeatured); + await DomainEvents.allSettled(); + + assert.equal((await collectionsService.getById(automaticFeaturedCollection.id))?.posts?.length, 3); + assert.equal((await collectionsService.getById(automaticNonFeaturedCollection.id))?.posts.length, 2); + assert.equal((await collectionsService.getById(manualCollection.id))?.posts.length, 2); + }); }); }); }); diff --git a/ghost/core/core/server/services/collections/model-to-domain-events-bridge.js b/ghost/core/core/server/services/collections/model-to-domain-events-bridge.js index 27f8fc9eff..4f73b269b9 100644 --- a/ghost/core/core/server/services/collections/model-to-domain-events-bridge.js +++ b/ghost/core/core/server/services/collections/model-to-domain-events-bridge.js @@ -2,7 +2,8 @@ const DomainEvents = require('@tryghost/domain-events'); const { CollectionResourceChangeEvent, PostDeletedEvent, - PostAddedEvent + PostAddedEvent, + PostEditedEvent } = require('@tryghost/collections'); const domainEventDispatcher = (modelEventName, data) => { @@ -18,8 +19,23 @@ const domainEventDispatcher = (modelEventName, data) => { event = PostAddedEvent.create({ id: data.id, featured: data.featured, + status: data.attributes.status, published_at: data.published_at }); + } if (modelEventName === 'post.edited') { + event = PostEditedEvent.create({ + id: data.id, + current: { + title: data.attributes.title, + status: data.attributes.status, + featured: data.attributes.featured, + published_at: data.attributes.published_at + }, + // @NOTE: this will need to represent the previous state of the post + // will be needed to optimize the query for the collection + previous: { + } + }); } else { event = CollectionResourceChangeEvent.create(modelEventName, change); }