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:
parent
6e224f3702
commit
bfefcfd4df
8 changed files with 135 additions and 9 deletions
|
@ -55,6 +55,10 @@ export class Collection {
|
|||
}
|
||||
}
|
||||
|
||||
removeAllPosts() {
|
||||
this._posts = [];
|
||||
}
|
||||
|
||||
private constructor(data: any) {
|
||||
this.id = data.id;
|
||||
this.title = data.title;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
18
ghost/collections/test/fixtures/PostsRepositoryInMemory.ts
vendored
Normal file
18
ghost/collections/test/fixtures/PostsRepositoryInMemory.ts
vendored
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
6
ghost/collections/test/fixtures/posts.ts
vendored
6
ghost/collections/test/fixtures/posts.ts
vendored
|
@ -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')
|
||||
}];
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
|
|
@ -351,7 +351,7 @@ describe('Collections API', function () {
|
|||
location: anyLocationFor('collections')
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
collections: [buildMatcher(0)]
|
||||
collections: [buildMatcher(2)]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue