0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Added automatic collection creation based on filter

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

- This implementation allows to create an automatic collection with a filter defining automatically populated posts that belong to a collection
- To populate collection using a filter the API client can send a `filter` property along with a collection request
- Filter values are compatible with the filters used in Content API (https://ghost.org/docs/content-api/#filter)
This commit is contained in:
Naz 2023-06-01 18:16:24 +07:00
parent 6e224f3702
commit bfefcfd4df
No known key found for this signature in database
8 changed files with 135 additions and 9 deletions

View file

@ -55,6 +55,10 @@ export class Collection {
}
}
removeAllPosts() {
this._posts = [];
}
private constructor(data: any) {
this.id = data.id;
this.title = data.title;

View file

@ -3,6 +3,7 @@ import {CollectionRepository} from './CollectionRepository';
type CollectionsServiceDeps = {
collectionsRepository: CollectionRepository;
postsRepository: IPostsRepository;
};
type CollectionPostDTO = {
@ -49,10 +50,17 @@ type CollectionPostInputDTO = {
published_at: Date;
};
type IPostsRepository = {
getAll(options: {filter?: string}): Promise<any[]>;
}
export class CollectionsService {
collectionsRepository: CollectionRepository;
postsRepository: IPostsRepository;
constructor(deps: CollectionsServiceDeps) {
this.collectionsRepository = deps.collectionsRepository;
this.postsRepository = deps.postsRepository;
}
toDTO(collection: Collection): CollectionDTO {
@ -102,6 +110,16 @@ export class CollectionsService {
featureImage: data.feature_image
});
if (collection.type === 'automatic' && collection.filter) {
const posts = await this.postsRepository.getAll({
filter: collection.filter
});
for (const post of posts) {
collection.addPost(post);
}
}
await this.collectionsRepository.save(collection);
return this.toDTO(collection);
@ -128,12 +146,24 @@ export class CollectionsService {
return null;
}
if (data.posts) {
if (collection.type === 'manual' && data.posts) {
for (const post of data.posts) {
collection.addPost(post);
}
}
if ((collection.type === 'automatic' || data.type === 'automatic') && data.filter) {
const posts = await this.postsRepository.getAll({
filter: data.filter
});
collection.removeAllPosts();
for (const post of posts) {
collection.addPost(post);
}
}
const collectionData = this.fromDTO(data);
Object.assign(collection, collectionData);

View file

@ -1,15 +1,36 @@
import assert from 'assert';
import {CollectionsService, CollectionsRepositoryInMemory, Collection} from '../src/index';
import {PostsRepositoryInMemory} from './fixtures/PostsRepositoryInMemory';
import {posts} from './fixtures/posts';
const initPostsRepository = (): PostsRepositoryInMemory => {
const postsRepository = new PostsRepositoryInMemory();
for (const post of posts) {
const collectionPost = {
id: post.id,
title: post.title,
slug: post.slug,
featured: post.featured,
published_at: post.published_at,
deleted: false
};
postsRepository.save(collectionPost);
}
return postsRepository;
};
describe('CollectionsService', function () {
let collectionsService: CollectionsService;
beforeEach(async function () {
const collectionsRepository = new CollectionsRepositoryInMemory();
const postsRepository = initPostsRepository();
collectionsService = new CollectionsService({
collectionsRepository
collectionsRepository,
postsRepository
});
});
@ -147,4 +168,40 @@ describe('CollectionsService', function () {
assert.equal(collection, null, 'Collection should be null');
});
});
describe('Automatic Collections', function () {
it('Can create an automatic collection', async function () {
const collection = await collectionsService.createCollection({
title: 'I am automatic',
description: 'testing automatic collection',
type: 'automatic',
filter: 'featured:true'
});
assert.equal(collection.type, 'automatic', 'Collection should be automatic');
assert.equal(collection.filter, 'featured:true', 'Collection should have the correct filter');
assert.equal(collection.posts.length, 1, 'Collection should have one post');
});
it('Populates collection when the type is changed to automatic and filter is present', async function () {
const collection = await collectionsService.createCollection({
title: 'I am automatic',
description: 'testing automatic collection',
type: 'manual'
});
assert.equal(collection.type, 'manual', 'Collection should be manual');
assert.equal(collection.posts.length, 0, 'Collection should have no posts');
const automaticCollection = await collectionsService.edit({
id: collection.id,
type: 'automatic',
filter: 'featured:true'
});
assert.equal(automaticCollection?.posts.length, 1, 'Collection should have one post');
assert.equal(automaticCollection?.posts[0].id, 'post-3-featured', 'Collection should have the correct post');
});
});
});

View file

@ -0,0 +1,18 @@
import {InMemoryRepository} from '@tryghost/in-memory-repository';
type CollectionPost = {
id: string;
featured: boolean;
published_at: Date;
deleted: boolean;
};
export class PostsRepositoryInMemory extends InMemoryRepository<string, CollectionPost> {
protected toPrimitive(entity: CollectionPost): object {
return {
id: entity.id,
featured: entity.featured,
published_at: entity.published_at
};
}
}

View file

@ -11,9 +11,9 @@ export const posts = [{
featured: false,
published_at: new Date('2023-04-05T07:20:07.447Z')
}, {
id: 'post-3',
title: 'Post 3',
slug: 'post-3',
id: 'post-3-featured',
title: 'Featured Post 3',
slug: 'featured-post-3',
featured: true,
published_at: new Date('2023-05-25T07:21:07.447Z')
}];

View file

@ -7,10 +7,18 @@ class CollectionsServiceWrapper {
api;
constructor() {
const models = require('../../models');
const collectionsRepositoryInMemory = new CollectionsRepositoryInMemory();
const collectionsService = new CollectionsService({
collectionsRepository: collectionsRepositoryInMemory
collectionsRepository: collectionsRepositoryInMemory,
postsRepository: {
getAll: async ({filter}) => {
return models.Post.findAll({
filter
});
}
}
});
this.api = {

View file

@ -9,7 +9,16 @@ Object {
"feature_image": null,
"filter": "featured:true",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"posts": Array [],
"posts": Array [
Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"sort_order": 0,
},
Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"sort_order": 1,
},
],
"title": "Test Collection",
"type": "automatic",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
@ -22,7 +31,7 @@ exports[`Collections API Automatic Collection Filtering Creates an automatic Col
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": "277",
"content-length": "374",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,

View file

@ -351,7 +351,7 @@ describe('Collections API', function () {
location: anyLocationFor('collections')
})
.matchBodySnapshot({
collections: [buildMatcher(0)]
collections: [buildMatcher(2)]
});
});
});