Handled updating Collections on TagDeletedEvent

closes https://github.com/TryGhost/Arch/issues/62

Because there are many ways in which filters can rely on tags, we will just
recalculate all automatic collections for now, rather than attempting to do
optimised updates.
This commit is contained in:
Fabien "egg" O'Carroll 2023-07-27 14:04:14 +01:00 committed by Fabien 'egg' O'Carroll
parent 9288f56649
commit fa40485fb1
2 changed files with 106 additions and 1 deletions

View File

@ -8,6 +8,7 @@ import {PostDeletedEvent} from './events/PostDeletedEvent';
import {PostAddedEvent} from './events/PostAddedEvent';
import {PostEditedEvent} from './events/PostEditedEvent';
import {RepositoryUniqueChecker} from './RepositoryUniqueChecker';
import {TagDeletedEvent} from './events/TagDeletedEvent';
const messages = {
cannotDeleteBuiltInCollectionError: {
@ -170,6 +171,36 @@ export class CollectionsService {
logging.info(`PostEditedEvent received, updating post ${event.data.id} in matching collections`);
await this.updatePostInMatchingCollections(event.data);
});
this.DomainEvents.subscribe(TagDeletedEvent, async (event: TagDeletedEvent) => {
logging.info(`TagDeletedEvent received for ${event.data.id}, updating all collections`);
await this.updateAllAutomaticCollections();
});
}
async updateAllAutomaticCollections(): Promise<void> {
return await this.collectionsRepository.createTransaction(async (transaction) => {
const collections = await this.collectionsRepository.getAll({
transaction
})
for (const collection of collections) {
if (collection.type === 'automatic' && collection.filter) {
collection.removeAllPosts();
const posts = await this.postsRepository.getAll({
filter: collection.filter,
transaction
});
for (const post of posts) {
collection.addPost(post);
}
await this.collectionsRepository.save(collection, {transaction});
}
}
});
}
async createCollection(data: CollectionInputDTO): Promise<CollectionDTO> {

View File

@ -5,7 +5,8 @@ import {
CollectionsRepositoryInMemory,
PostDeletedEvent,
PostAddedEvent,
PostEditedEvent
PostEditedEvent,
TagDeletedEvent
} from '../src/index';
import {PostsRepositoryInMemory} from './fixtures/PostsRepositoryInMemory';
import {posts as postFixtures} from './fixtures/posts';
@ -311,6 +312,79 @@ describe('CollectionsService', function () {
await collectionsService.destroy(manualCollection.id);
});
it('Updates all automatic collections when a tag is deleted', async function () {
const collectionsRepository = new CollectionsRepositoryInMemory();
postsRepository = initPostsRepository([
{
id: 'post-1',
url: 'http://localhost:2368/post-1/',
title: 'Post 1',
slug: 'post-1',
featured: false,
tags: [{slug: 'to-be-deleted'}, {slug: 'other-tag'}],
created_at: new Date('2023-03-15T07:19:07.447Z'),
updated_at: new Date('2023-03-15T07:19:07.447Z'),
published_at: new Date('2023-03-15T07:19:07.447Z')
}, {
id: 'post-2',
url: 'http://localhost:2368/post-2/',
title: 'Post 2',
slug: 'post-2',
featured: false,
tags: [{slug: 'to-be-deleted'}, {slug: 'other-tag'}],
created_at: new Date('2023-04-05T07:20:07.447Z'),
updated_at: new Date('2023-04-05T07:20:07.447Z'),
published_at: new Date('2023-04-05T07:20:07.447Z')
}
]);
collectionsService = new CollectionsService({
collectionsRepository,
postsRepository,
DomainEvents,
slugService: {
async generate(input) {
return input.replace(/\s+/g, '-').toLowerCase();
}
}
});
const automaticCollectionWithTag = await collectionsService.createCollection({
title: 'Automatic Collection with Tag',
description: 'testing automatic collection with tag',
type: 'automatic',
filter: 'tags:to-be-deleted'
});
const automaticCollectionWithoutTag = await collectionsService.createCollection({
title: 'Automatic Collection without Tag',
description: 'testing automatic collection without tag',
type: 'automatic',
filter: 'tags:other-tag'
});
assert.equal((await collectionsService.getById(automaticCollectionWithTag.id))?.posts.length, 2);
assert.equal((await collectionsService.getById(automaticCollectionWithoutTag.id))?.posts.length, 2);
collectionsService.subscribeToEvents();
const tagDeletedEvent = TagDeletedEvent.create({
id: 'to-be-deleted'
});
const posts = await postsRepository.getAll();
for (const post of posts) {
post.tags = post.tags.filter(tag => tag.slug !== 'to-be-deleted');
await postsRepository.save(post);
}
DomainEvents.dispatch(tagDeletedEvent);
await DomainEvents.allSettled();
assert.equal((await collectionsService.getById(automaticCollectionWithTag.id))?.posts.length, 0);
assert.equal((await collectionsService.getById(automaticCollectionWithoutTag.id))?.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);