mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-08 02:52:39 -05:00
Added tag filter support to collections
refs https://github.com/TryGhost/Arch/issues/41 - When an new collection is created the relational "tags" filter is now picked up properly and appropriate posts matching the tag filter are assigned and stored in the collection. Example collection filter that is now supported: `tags:['bacon']` - Additionally cleaned up returned collection post DTOs, so we return as little data as possible and add only the fields that are needed
This commit is contained in:
parent
939a8fef33
commit
53f9f954c1
8 changed files with 207 additions and 19 deletions
|
@ -37,6 +37,7 @@
|
|||
},
|
||||
"c8": {
|
||||
"exclude": [
|
||||
"src/CollectionPost.ts",
|
||||
"src/CollectionRepository.ts",
|
||||
"src/UniqueChecker.ts",
|
||||
"src/**/*.d.ts",
|
||||
|
|
|
@ -3,5 +3,5 @@ export type CollectionPost = {
|
|||
id: string;
|
||||
featured?: boolean;
|
||||
published_at?: Date;
|
||||
tags: string[];
|
||||
tags?: string[];
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ import ObjectID from 'bson-objectid';
|
|||
import {Collection} from './Collection';
|
||||
import {CollectionResourceChangeEvent} from './CollectionResourceChangeEvent';
|
||||
import {CollectionRepository} from './CollectionRepository';
|
||||
import {CollectionPost} from './CollectionPost';
|
||||
import {MethodNotAllowedError, NotFoundError} from '@tryghost/errors';
|
||||
import {PostDeletedEvent} from './events/PostDeletedEvent';
|
||||
import {PostAddedEvent} from './events/PostAddedEvent';
|
||||
|
@ -41,11 +42,15 @@ type CollectionPostDTO = {
|
|||
|
||||
type CollectionPostListItemDTO = {
|
||||
id: string;
|
||||
url: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
featured: boolean;
|
||||
featured_image?: string;
|
||||
published_at: Date
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
published_at: Date,
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
type ManualCollection = {
|
||||
|
@ -130,6 +135,21 @@ export class CollectionsService {
|
|||
};
|
||||
}
|
||||
|
||||
private toCollectionPostDTO(post: any): CollectionPostListItemDTO {
|
||||
return {
|
||||
id: post.id,
|
||||
url: post.url,
|
||||
slug: post.slug,
|
||||
title: post.title,
|
||||
featured: post.featured,
|
||||
featured_image: post.featured_image,
|
||||
created_at: post.created_at,
|
||||
updated_at: post.updated_at,
|
||||
published_at: post.published_at,
|
||||
tags: post.tags
|
||||
};
|
||||
}
|
||||
|
||||
private fromDTO(data: any): any {
|
||||
const mappedDTO: {[index: string]:any} = {
|
||||
title: data.title,
|
||||
|
@ -244,7 +264,7 @@ export class CollectionsService {
|
|||
}
|
||||
}
|
||||
|
||||
private async addPostToMatchingCollections(post: {id: string, featured: boolean, published_at: Date}) {
|
||||
private async addPostToMatchingCollections(post: CollectionPost) {
|
||||
const collections = await this.collectionsRepository.getAll({
|
||||
filter: 'type:automatic'
|
||||
});
|
||||
|
@ -382,7 +402,7 @@ export class CollectionsService {
|
|||
const posts = await this.postsRepository.getBulk(postIds);
|
||||
|
||||
return {
|
||||
data: posts,
|
||||
data: posts.map(this.toCollectionPostDTO),
|
||||
meta: {
|
||||
pagination: {
|
||||
page: page,
|
||||
|
|
|
@ -2,6 +2,7 @@ type PostData = {
|
|||
id: string;
|
||||
featured: boolean;
|
||||
published_at: Date;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
export class PostAddedEvent {
|
||||
|
|
12
ghost/collections/test/fixtures/posts.ts
vendored
12
ghost/collections/test/fixtures/posts.ts
vendored
|
@ -1,25 +1,37 @@
|
|||
export const posts = [{
|
||||
id: 'post-1',
|
||||
url: 'http://localhost:2368/post-1/',
|
||||
title: 'Post 1',
|
||||
slug: 'post-1',
|
||||
featured: false,
|
||||
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,
|
||||
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')
|
||||
}, {
|
||||
id: 'post-3-featured',
|
||||
url: 'http://localhost:2368/featured-post-3/',
|
||||
title: 'Featured Post 3',
|
||||
slug: 'featured-post-3',
|
||||
featured: true,
|
||||
created_at: new Date('2023-05-25T07:21:07.447Z'),
|
||||
updated_at: new Date('2023-05-25T07:21:07.447Z'),
|
||||
published_at: new Date('2023-05-25T07:21:07.447Z')
|
||||
}, {
|
||||
id: 'post-4-featured',
|
||||
url: 'http://localhost:2368/featured-post-4/',
|
||||
title: 'Featured Post 4',
|
||||
slug: 'featured-post-4',
|
||||
featured: true,
|
||||
created_at: new Date('2023-05-15T07:21:07.447Z'),
|
||||
updated_at: new Date('2023-05-15T07:21:07.447Z'),
|
||||
published_at: new Date('2023-05-15T07:21:07.447Z')
|
||||
}];
|
||||
|
|
|
@ -27,17 +27,31 @@ class PostsRepository {
|
|||
return attrs;
|
||||
}
|
||||
|
||||
serializeTags(attrs) {
|
||||
if (attrs.tags) {
|
||||
attrs.tags = attrs.tags.map(tag => tag.slug);
|
||||
}
|
||||
|
||||
if (attrs.primary_tag) {
|
||||
delete attrs.primary_tag;
|
||||
}
|
||||
|
||||
return attrs;
|
||||
}
|
||||
|
||||
async getAll({filter}) {
|
||||
const response = await this.browsePostsAPI({
|
||||
options: {
|
||||
filter: `(${filter})+type:post`,
|
||||
status: 'all',
|
||||
limit: 'all'
|
||||
limit: 'all',
|
||||
withRelated: ['tags']
|
||||
}
|
||||
});
|
||||
|
||||
response.posts = response.posts
|
||||
.map(this.serializeDates.bind(this));
|
||||
.map(this.serializeDates.bind(this))
|
||||
.map(this.serializeTags.bind(this));
|
||||
|
||||
return response.posts;
|
||||
}
|
||||
|
@ -46,12 +60,14 @@ class PostsRepository {
|
|||
const response = await this.browsePostsAPI({
|
||||
options: {
|
||||
filter: `id:[${ids.join(',')}]+type:post`,
|
||||
status: 'all'
|
||||
status: 'all',
|
||||
withRelated: ['tags']
|
||||
}
|
||||
});
|
||||
|
||||
response.posts = response.posts
|
||||
.map(this.serializeDates.bind(this));
|
||||
.map(this.serializeDates.bind(this))
|
||||
.map(this.serializeTags.bind(this));
|
||||
|
||||
return response.posts;
|
||||
}
|
||||
|
|
|
@ -114,6 +114,107 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Collections API Automatic Collection Filtering Creates an automatic Collection with a tag filter 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": "BACON!",
|
||||
"feature_image": null,
|
||||
"filter": "tags:['bacon']",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"posts": Array [
|
||||
Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"sort_order": 0,
|
||||
},
|
||||
Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"sort_order": 1,
|
||||
},
|
||||
],
|
||||
"slug": "bacon",
|
||||
"title": "Test Collection with tag filter",
|
||||
"type": "automatic",
|
||||
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Collections API Automatic Collection Filtering Creates an automatic Collection with a tag filter 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": "385",
|
||||
"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 Automatic Collection Filtering Creates an automatic Collection with a tag filter 3: [body] 1`] = `
|
||||
Object {
|
||||
"collection_posts": Array [
|
||||
Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
|
||||
"featured": false,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
|
||||
"slug": "ghostly-kitchen-sink",
|
||||
"tags": Array [
|
||||
"kitchen-sink",
|
||||
"bacon",
|
||||
],
|
||||
"title": "Ghostly Kitchen Sink",
|
||||
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
|
||||
"url": StringMatching /http:\\\\/\\\\/127\\.0\\.0\\.1:2369\\\\/\\[A-Za-z0-9_-\\]\\+\\\\//,
|
||||
},
|
||||
Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
|
||||
"featured": false,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
|
||||
"slug": "html-ipsum",
|
||||
"tags": Array [
|
||||
"kitchen-sink",
|
||||
"bacon",
|
||||
],
|
||||
"title": "HTML Ipsum",
|
||||
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/,
|
||||
"url": StringMatching /http:\\\\/\\\\/127\\.0\\.0\\.1:2369\\\\/\\[A-Za-z0-9_-\\]\\+\\\\//,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"limit": 15,
|
||||
"next": null,
|
||||
"page": 1,
|
||||
"pages": 1,
|
||||
"prev": null,
|
||||
"total": 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Collections API Automatic Collection Filtering Creates an automatic Collection with a tag filter 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": "746",
|
||||
"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 Browse Can browse Collections 1: [body] 1`] = `
|
||||
Object {
|
||||
"collections": Array [
|
||||
|
|
|
@ -12,10 +12,9 @@ const {
|
|||
anyLocationFor,
|
||||
anyObjectId,
|
||||
anyISODateTime,
|
||||
// anyISODateTimeWithTZ,
|
||||
anyISODateTimeWithTZ,
|
||||
anyNumber,
|
||||
// anyUuid,
|
||||
// anyLocalURL,
|
||||
anyLocalURL,
|
||||
anyString
|
||||
} = matchers;
|
||||
|
||||
|
@ -25,14 +24,13 @@ const matchCollection = {
|
|||
updated_at: anyISODateTime
|
||||
};
|
||||
|
||||
// const matchCollectionPost = {
|
||||
// id: anyObjectId,
|
||||
// url: anyLocalURL,
|
||||
// created_at: anyISODateTimeWithTZ,
|
||||
// updated_at: anyISODateTimeWithTZ,
|
||||
// published_at: anyISODateTimeWithTZ,
|
||||
// uuid: anyUuid
|
||||
// };
|
||||
const matchCollectionPost = {
|
||||
id: anyObjectId,
|
||||
url: anyLocalURL,
|
||||
created_at: anyISODateTimeWithTZ,
|
||||
updated_at: anyISODateTimeWithTZ,
|
||||
published_at: anyISODateTimeWithTZ
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -373,5 +371,44 @@ describe('Collections API', function () {
|
|||
collections: [buildMatcher(9)]
|
||||
});
|
||||
});
|
||||
|
||||
it('Creates an automatic Collection with a tag filter', async function () {
|
||||
const collection = {
|
||||
title: 'Test Collection with tag filter',
|
||||
slug: 'bacon',
|
||||
description: 'BACON!',
|
||||
type: 'automatic',
|
||||
filter: 'tags:[\'bacon\']'
|
||||
};
|
||||
|
||||
await agent
|
||||
.post('/collections/')
|
||||
.body({
|
||||
collections: [collection]
|
||||
})
|
||||
.expectStatus(201)
|
||||
.matchHeaderSnapshot({
|
||||
'content-version': anyContentVersion,
|
||||
etag: anyEtag,
|
||||
location: anyLocationFor('collections')
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
collections: [buildMatcher(2)]
|
||||
});
|
||||
|
||||
await agent
|
||||
.get('/collections/bacon/posts/')
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
'content-version': anyContentVersion,
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
collection_posts: [
|
||||
matchCollectionPost,
|
||||
matchCollectionPost
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue