Optimized handling for post.deleted event in collections

refs https://github.com/TryGhost/Team/issues/3428

- I'm taking an approach of adding specialized support for each event one-by-one.
- The post resource deletion event is the most straight forward and works same for both types of collections.
This commit is contained in:
Naz 2023-06-20 17:03:36 +07:00 committed by naz
parent 2c6f30b4b8
commit 49d831d971
6 changed files with 122 additions and 27 deletions

View File

@ -67,6 +67,10 @@ export class Collection {
}
}
includesPost(id: string) {
return this.posts.includes(id);
}
removeAllPosts() {
this._posts = [];
}

View File

@ -4,5 +4,5 @@ export interface CollectionRepository {
save(collection: Collection): Promise<void>
getById(id: string): Promise<Collection | null>
getBySlug(slug: string): Promise<Collection | null>
getAll(options: any): Promise<Collection[]>
getAll(options?: any): Promise<Collection[]>
}

View File

@ -0,0 +1,21 @@
type CollectionResourceChangeEventData = {
id: string;
resource: 'post' | 'tag' | 'author';
[any: string]: any;
};
export class CollectionResourceChangeEvent {
name: string;
data: CollectionResourceChangeEventData;
timestamp: Date;
constructor(name: string, data: CollectionResourceChangeEventData, timestamp: Date) {
this.name = name;
this.data = data;
this.timestamp = timestamp;
}
static create(name: string, data: CollectionResourceChangeEventData, timestamp = new Date()) {
return new CollectionResourceChangeEvent(name, data, timestamp);
}
}

View File

@ -1,4 +1,5 @@
import {Collection} from './Collection';
import {CollectionResourceChangeEvent} from './CollectionResourceChangeEvent';
import {CollectionRepository} from './CollectionRepository';
import tpl from '@tryghost/tpl';
import {MethodNotAllowedError, NotFoundError} from '@tryghost/errors';
@ -187,14 +188,25 @@ export class CollectionsService {
}
}
async updateAutomaticCollections() {
const collections = await this.collectionsRepository.getAll({
filter: 'type:automatic'
});
async updateCollections(event: CollectionResourceChangeEvent) {
if (event.name === 'post.deleted') {
// NOTE: 'delete' works the same for both manual and automatic collections
const collections = await this.collectionsRepository.getAll();
for (const collection of collections) {
await this.updateAutomaticCollectionItems(collection);
await this.collectionsRepository.save(collection);
for (const collection of collections) {
if (collection.includesPost(event.data.id)) {
await collection.removePost(event.data.id);
}
}
} else {
const collections = await this.collectionsRepository.getAll({
filter: 'type:automatic'
});
for (const collection of collections) {
await this.updateAutomaticCollectionItems(collection);
await this.collectionsRepository.save(collection);
}
}
}

View File

@ -1,3 +1,4 @@
export * from './CollectionsService';
export * from './CollectionsRepositoryInMemory';
export * from './Collection';
export * from './CollectionResourceChangeEvent';

View File

@ -1,5 +1,9 @@
import assert from 'assert';
import {CollectionsService, CollectionsRepositoryInMemory} from '../src/index';
import {
CollectionsService,
CollectionsRepositoryInMemory,
CollectionResourceChangeEvent
} from '../src/index';
import {PostsRepositoryInMemory} from './fixtures/PostsRepositoryInMemory';
import {posts} from './fixtures/posts';
@ -23,10 +27,11 @@ const initPostsRepository = (): PostsRepositoryInMemory => {
describe('CollectionsService', function () {
let collectionsService: CollectionsService;
let postsRepository: PostsRepositoryInMemory;
beforeEach(async function () {
const collectionsRepository = new CollectionsRepositoryInMemory();
const postsRepository = initPostsRepository();
postsRepository = initPostsRepository();
collectionsService = new CollectionsService({
collectionsRepository,
@ -311,29 +316,81 @@ describe('CollectionsService', function () {
assert.equal(updatedCollection?.posts[0].id, 'post-2', 'Collection should have the correct post');
});
// @NOTE: add a more comprehensive test as this one is too basic
it('Updates all automatic collections', async function () {
let collection1 = await collectionsService.createCollection({
title: 'Featured Collection 1',
description: 'testing automatic collection',
type: 'automatic',
filter: 'featured:true'
describe('updateCollections', function () {
let automaticFeaturedCollection: any;
let automaticNonFeaturedCollection: any;
let manualCollection: any;
beforeEach(async function () {
automaticFeaturedCollection = await collectionsService.createCollection({
title: 'Featured Collection',
description: 'testing automatic collection',
type: 'automatic',
filter: 'featured:true'
});
automaticNonFeaturedCollection = await collectionsService.createCollection({
title: 'Non-Featured Collection',
description: 'testing automatic collection',
type: 'automatic',
filter: 'featured:false'
});
manualCollection = await collectionsService.createCollection({
title: 'Manual Collection',
description: 'testing manual collection',
type: 'manual'
});
await collectionsService.addPostToCollection(manualCollection.id, posts[0]);
await collectionsService.addPostToCollection(manualCollection.id, posts[1]);
});
let collection2 = await collectionsService.createCollection({
title: 'Featured Collection 2',
description: 'testing automatic collection',
type: 'automatic',
filter: 'featured:true'
afterEach(async function () {
await collectionsService.destroy(automaticFeaturedCollection.id);
await collectionsService.destroy(automaticNonFeaturedCollection.id);
await collectionsService.destroy(manualCollection.id);
});
assert.equal(collection1.posts.length, 2);
assert.equal(collection2.posts.length, 2);
it('Updates all collections when post is deleted', async function () {
assert.equal((await collectionsService.getById(automaticFeaturedCollection.id))?.posts?.length, 2);
assert.equal((await collectionsService.getById(automaticNonFeaturedCollection.id))?.posts.length, 2);
assert.equal((await collectionsService.getById(manualCollection.id))?.posts.length, 2);
await collectionsService.updateAutomaticCollections();
const updateCollectionEvent = CollectionResourceChangeEvent.create('post.deleted', {
id: posts[0].id,
resource: 'post'
});
assert.equal(collection1.posts.length, 2);
assert.equal(collection2.posts.length, 2);
await collectionsService.updateCollections(updateCollectionEvent);
assert.equal((await collectionsService.getById(automaticFeaturedCollection.id))?.posts?.length, 2);
assert.equal((await collectionsService.getById(automaticNonFeaturedCollection.id))?.posts.length, 1);
assert.equal((await collectionsService.getById(manualCollection.id))?.posts.length, 1);
});
it('Updates automatic collections only when post is published', async function () {
const newPost = {
id: 'post-published',
title: 'Post Published',
slug: 'post-published',
featured: true,
published_at: new Date('2023-03-16T07:19:07.447Z'),
deleted: false
};
await postsRepository.save(newPost);
const updateCollectionEvent = CollectionResourceChangeEvent.create('post.published', {
id: newPost.id,
resource: 'post'
});
await collectionsService.updateCollections(updateCollectionEvent);
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);
});
});
});
});