Added collections update after bulk adding tags
refs https://github.com/TryGhost/Arch/issues/77 - During initial development we have missed to support collections update when tags are added to posts in bulk. It's especially valid usecase since we can define automatic collection with a filter containing not yet existing tags.
This commit is contained in:
parent
62d5ca558d
commit
acccc16614
@ -4,7 +4,8 @@ import {Knex} from "knex";
|
||||
import {
|
||||
PostsBulkUnpublishedEvent,
|
||||
PostsBulkFeaturedEvent,
|
||||
PostsBulkUnfeaturedEvent
|
||||
PostsBulkUnfeaturedEvent,
|
||||
PostsBulkAddTagsEvent
|
||||
} from "@tryghost/post-events";
|
||||
import {Collection} from './Collection';
|
||||
import {CollectionRepository} from './CollectionRepository';
|
||||
@ -207,6 +208,11 @@ export class CollectionsService {
|
||||
logging.info(`TagDeletedEvent received for ${event.data.id}, updating all collections`);
|
||||
await this.updateAllAutomaticCollections();
|
||||
});
|
||||
|
||||
this.DomainEvents.subscribe(PostsBulkAddTagsEvent, async (event: PostsBulkAddTagsEvent) => {
|
||||
logging.info(`PostsBulkAddTagsEvent received for ${event.data}, updating all collections`);
|
||||
await this.updateAllAutomaticCollections();
|
||||
});
|
||||
}
|
||||
|
||||
async updateAllAutomaticCollections(): Promise<void> {
|
||||
|
@ -12,7 +12,8 @@ import {
|
||||
PostsBulkDestroyedEvent,
|
||||
PostsBulkUnpublishedEvent,
|
||||
PostsBulkFeaturedEvent,
|
||||
PostsBulkUnfeaturedEvent
|
||||
PostsBulkUnfeaturedEvent,
|
||||
PostsBulkAddTagsEvent
|
||||
} from '@tryghost/post-events';
|
||||
import {PostsRepositoryInMemory} from './fixtures/PostsRepositoryInMemory';
|
||||
import {posts as postFixtures} from './fixtures/posts';
|
||||
@ -392,6 +393,81 @@ describe('CollectionsService', function () {
|
||||
assert.equal((await collectionsService.getById(automaticCollectionWithoutTag.id))?.posts.length, 2);
|
||||
});
|
||||
|
||||
it('Updates all collections when post tags are added in bulk', async function () {
|
||||
const collectionsRepository = new CollectionsRepositoryInMemory();
|
||||
postsRepository = await initPostsRepository([
|
||||
{
|
||||
id: 'post-1',
|
||||
url: 'http://localhost:2368/post-1/',
|
||||
title: 'Post 1',
|
||||
slug: 'post-1',
|
||||
featured: false,
|
||||
tags: [{slug: 'existing-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: [],
|
||||
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 automaticCollectionWithExistingTag = await collectionsService.createCollection({
|
||||
title: 'Automatic Collection with Tag',
|
||||
description: 'testing automatic collection with tag',
|
||||
type: 'automatic',
|
||||
filter: 'tags:existing-tag'
|
||||
});
|
||||
|
||||
const automaticCollectionWithBulkAddedTag = await collectionsService.createCollection({
|
||||
title: 'Automatic Collection without Tag',
|
||||
description: 'testing automatic collection without tag',
|
||||
type: 'automatic',
|
||||
filter: 'tags:to-be-created'
|
||||
});
|
||||
|
||||
assert.equal((await collectionsService.getById(automaticCollectionWithExistingTag.id))?.posts.length, 1);
|
||||
assert.equal((await collectionsService.getById(automaticCollectionWithBulkAddedTag.id))?.posts.length, 0);
|
||||
|
||||
collectionsService.subscribeToEvents();
|
||||
|
||||
const posts = await postsRepository.getAll();
|
||||
|
||||
for (const post of posts) {
|
||||
post.tags.push({slug: 'to-be-created'});
|
||||
await postsRepository.save(post);
|
||||
}
|
||||
|
||||
const postsBulkAddTagsEvent = PostsBulkAddTagsEvent.create([
|
||||
'post-1',
|
||||
'post-2'
|
||||
]);
|
||||
|
||||
DomainEvents.dispatch(postsBulkAddTagsEvent);
|
||||
await DomainEvents.allSettled();
|
||||
|
||||
assert.equal((await collectionsService.getById(automaticCollectionWithExistingTag.id))?.posts.length, 1);
|
||||
assert.equal((await collectionsService.getById(automaticCollectionWithBulkAddedTag.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);
|
||||
|
@ -1365,6 +1365,120 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Collections API Collection Posts updates automatically Updates a collection with tag filter when tag is added to posts in bulk 1: [body] 1`] = `
|
||||
Object {
|
||||
"collections": Array [
|
||||
Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"description": null,
|
||||
"feature_image": null,
|
||||
"filter": "tags:['papaya']",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"slug": "papaya-madness",
|
||||
"title": "Papaya madness",
|
||||
"type": "automatic",
|
||||
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Collections API Collection Posts updates automatically Updates a collection with tag filter when tag is added to posts in bulk 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "266",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/collections\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-cache-invalidate": "/*",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Collections API Collection Posts updates automatically Updates a collection with tag filter when tag is added to posts in bulk 3: [body] 1`] = `
|
||||
Object {
|
||||
"collections": Array [
|
||||
Object {
|
||||
"count": Object {
|
||||
"posts": 0,
|
||||
},
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"description": null,
|
||||
"feature_image": null,
|
||||
"filter": "tags:['papaya']",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"slug": "papaya-madness",
|
||||
"title": "Papaya madness",
|
||||
"type": "automatic",
|
||||
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Collections API Collection Posts updates automatically Updates a collection with tag filter when tag is added to posts in bulk 4: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "286",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Collections API Collection Posts updates automatically Updates a collection with tag filter when tag is added to posts in bulk 5: [body] 1`] = `
|
||||
Object {
|
||||
"bulk": Object {
|
||||
"meta": Object {
|
||||
"stats": Object {
|
||||
"successful": 11,
|
||||
"unsuccessful": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Collections API Collection Posts updates automatically Updates a collection with tag filter when tag is added to posts in bulk 6: [body] 1`] = `
|
||||
Object {
|
||||
"collections": Array [
|
||||
Object {
|
||||
"count": Object {
|
||||
"posts": 11,
|
||||
},
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"description": null,
|
||||
"feature_image": null,
|
||||
"filter": "tags:['papaya']",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"slug": "papaya-madness",
|
||||
"title": "Papaya madness",
|
||||
"type": "automatic",
|
||||
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Collections API Collection Posts updates automatically Updates a collection with tag filter when tag is added to posts in bulk 7: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "287",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Collections API Collection Posts updates automatically Updates collections when a Post is added/edited/deleted 1: [body] 1`] = `
|
||||
Object {
|
||||
"collections": Array [
|
||||
|
@ -1,11 +1,11 @@
|
||||
const assert = require('assert/strict');
|
||||
const DomainEvents = require('@tryghost/domain-events');
|
||||
const {
|
||||
agentProvider,
|
||||
fixtureManager,
|
||||
mockManager,
|
||||
matchers
|
||||
} = require('../../utils/e2e-framework');
|
||||
const DomainEvents = require('@tryghost/domain-events/lib/DomainEvents');
|
||||
const {
|
||||
anyContentVersion,
|
||||
anyEtag,
|
||||
@ -583,5 +583,94 @@ describe('Collections API', function () {
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
it('Updates a collection with tag filter when tag is added to posts in bulk', async function (){
|
||||
const collection = {
|
||||
title: 'Papaya madness',
|
||||
type: 'automatic',
|
||||
filter: 'tags:[\'papaya\']'
|
||||
};
|
||||
|
||||
const {body: {collections: [{id: collectionId}]}} = await agent
|
||||
.post('/collections/')
|
||||
.body({
|
||||
collections: [collection]
|
||||
})
|
||||
.expectStatus(201)
|
||||
.matchHeaderSnapshot({
|
||||
'content-version': anyContentVersion,
|
||||
etag: anyEtag,
|
||||
location: anyLocationFor('collections')
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
collections: [matchCollection]
|
||||
});
|
||||
|
||||
// should contain no posts
|
||||
await agent
|
||||
.get(`/collections/${collectionId}/?include=count.posts`)
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
'content-version': anyContentVersion,
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
collections: [{
|
||||
...matchCollection,
|
||||
count: {
|
||||
posts: 0
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
const tag = {
|
||||
name: 'Papaya',
|
||||
slug: 'papaya'
|
||||
};
|
||||
|
||||
const {body: {tags: [{id: tagId}]}} = await agent
|
||||
.post('/tags/')
|
||||
.body({
|
||||
tags: [tag]
|
||||
})
|
||||
.expectStatus(201);
|
||||
|
||||
// add papaya tag to all posts
|
||||
await agent
|
||||
.put('/posts/bulk/?filter=' + encodeURIComponent('status:[published]'))
|
||||
.body({
|
||||
bulk: {
|
||||
action: 'addTag',
|
||||
meta: {
|
||||
tags: [
|
||||
{
|
||||
id: tagId
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
.expectStatus(200)
|
||||
.matchBodySnapshot();
|
||||
|
||||
await DomainEvents.allSettled();
|
||||
|
||||
// should contain published posts with papaya tags
|
||||
await agent
|
||||
.get(`/collections/${collectionId}/?include=count.posts`)
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
'content-version': anyContentVersion,
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
collections: [{
|
||||
...matchCollection,
|
||||
count: {
|
||||
posts: 11
|
||||
}
|
||||
}]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
13
ghost/post-events/src/PostsBulkAddTagsEvent.ts
Normal file
13
ghost/post-events/src/PostsBulkAddTagsEvent.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export class PostsBulkAddTagsEvent {
|
||||
data: string[];
|
||||
timestamp: Date;
|
||||
|
||||
constructor(data: string[], timestamp: Date) {
|
||||
this.data = data;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
static create(data: string[], timestamp = new Date()) {
|
||||
return new PostsBulkAddTagsEvent(data, timestamp);
|
||||
}
|
||||
}
|
@ -2,3 +2,4 @@ export * from './PostsBulkDestroyedEvent';
|
||||
export * from './PostsBulkUnpublishedEvent';
|
||||
export * from './PostsBulkFeaturedEvent';
|
||||
export * from './PostsBulkUnfeaturedEvent';
|
||||
export * from './PostsBulkAddTagsEvent';
|
||||
|
@ -3,7 +3,8 @@ import {
|
||||
PostsBulkDestroyedEvent,
|
||||
PostsBulkUnpublishedEvent,
|
||||
PostsBulkFeaturedEvent,
|
||||
PostsBulkUnfeaturedEvent
|
||||
PostsBulkUnfeaturedEvent,
|
||||
PostsBulkAddTagsEvent
|
||||
} from '../src/index';
|
||||
|
||||
describe('Post Events', function () {
|
||||
@ -30,4 +31,10 @@ describe('Post Events', function () {
|
||||
assert.ok(event);
|
||||
assert.equal(event.data.length, 3);
|
||||
});
|
||||
|
||||
it('Can instantiate PostsBulkAddTagsEvent', function () {
|
||||
const event = PostsBulkAddTagsEvent.create(['1', '2', '3']);
|
||||
assert.ok(event);
|
||||
assert.equal(event.data.length, 3);
|
||||
});
|
||||
});
|
||||
|
@ -4,12 +4,13 @@ const tpl = require('@tryghost/tpl');
|
||||
const errors = require('@tryghost/errors');
|
||||
const ObjectId = require('bson-objectid').default;
|
||||
const pick = require('lodash/pick');
|
||||
const DomainEvents = require('@tryghost/domain-events/lib/DomainEvents');
|
||||
const DomainEvents = require('@tryghost/domain-events');
|
||||
const {
|
||||
PostsBulkDestroyedEvent,
|
||||
PostsBulkUnpublishedEvent,
|
||||
PostsBulkFeaturedEvent,
|
||||
PostsBulkUnfeaturedEvent
|
||||
PostsBulkUnfeaturedEvent,
|
||||
PostsBulkAddTagsEvent
|
||||
} = require('@tryghost/post-events');
|
||||
|
||||
const messages = {
|
||||
@ -319,6 +320,9 @@ class PostsService {
|
||||
await options.transacting('posts_tags').insert(postTags);
|
||||
await this.models.Post.addActions('edited', postRows.map(p => p.id), options);
|
||||
|
||||
const event = PostsBulkAddTagsEvent.create(postTags.map(pt => pt.post_id));
|
||||
DomainEvents.dispatch(event);
|
||||
|
||||
return {
|
||||
successful: postRows.length,
|
||||
unsuccessful: 0
|
||||
|
Loading…
Reference in New Issue
Block a user