mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 03:44:29 +03:00
Added Posts relation support to collections package
refs https://github.com/TryGhost/Team/issues/3260 - Adds "posts" relation to Collection entity to manage posts belonging to the collection
This commit is contained in:
parent
c7954fa695
commit
66d489b8b3
@ -4,6 +4,7 @@ import {ValidationError} from '@tryghost/errors';
|
||||
import tpl from '@tryghost/tpl';
|
||||
|
||||
import ObjectID from 'bson-objectid';
|
||||
import {PostDTO} from './PostDTO';
|
||||
|
||||
const messages = {
|
||||
invalidIDProvided: 'Invalid ID provided for Collection',
|
||||
@ -22,6 +23,8 @@ export class Collection {
|
||||
updatedAt: Date;
|
||||
deleted: boolean;
|
||||
|
||||
posts: PostDTO[];
|
||||
|
||||
private constructor(data: any) {
|
||||
this.id = data.id;
|
||||
this.title = data.title;
|
||||
@ -33,6 +36,7 @@ export class Collection {
|
||||
this.createdAt = data.createdAt;
|
||||
this.updatedAt = data.updatedAt;
|
||||
this.deleted = data.deleted;
|
||||
this.posts = data.posts;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
@ -45,7 +49,12 @@ export class Collection {
|
||||
filter: this.filter,
|
||||
featureImage: this.featureImage,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt
|
||||
updatedAt: this.updatedAt,
|
||||
posts: this.posts.map(post => ({
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
slug: post.slug
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
@ -87,7 +96,8 @@ export class Collection {
|
||||
featureImage: data.feature_image || null,
|
||||
createdAt: Collection.validateDateField(data.created_at, 'created_at'),
|
||||
updatedAt: Collection.validateDateField(data.updated_at, 'updated_at'),
|
||||
deleted: data.deleted || false
|
||||
deleted: data.deleted || false,
|
||||
posts: data.posts || []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,41 +1,65 @@
|
||||
import {Collection} from './Collection';
|
||||
import {PostDTO} from './PostDTO';
|
||||
|
||||
type CollectionsServiceDeps = {
|
||||
repository: any;
|
||||
collectionsRepository: any;
|
||||
postsRepository: {
|
||||
getBulk: any;
|
||||
}
|
||||
};
|
||||
|
||||
export class CollectionsService {
|
||||
repository: any;
|
||||
collectionsRepository: any;
|
||||
postsRepository: any;
|
||||
|
||||
constructor(deps: CollectionsServiceDeps) {
|
||||
this.repository = deps.repository;
|
||||
this.collectionsRepository = deps.collectionsRepository;
|
||||
this.postsRepository = deps.postsRepository;
|
||||
}
|
||||
|
||||
async save(data: any): Promise<Collection> {
|
||||
const collection = await Collection.create(data);
|
||||
await this.repository.save(collection);
|
||||
await this.collectionsRepository.save(collection);
|
||||
return collection;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param collection to add tags to
|
||||
* @param postIds
|
||||
*/
|
||||
private async addPosts(collection: Collection, postDTOs: PostDTO[]) : Promise<Collection> {
|
||||
const postIds = postDTOs.map(post => post.id);
|
||||
const posts = await this.postsRepository.getBulk(postIds);
|
||||
|
||||
collection.posts = posts.map((post: any) => post.id);
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
async edit(data: any): Promise<Collection | null> {
|
||||
const collection = await this.repository.getById(data.id);
|
||||
const collection = await this.collectionsRepository.getById(data.id);
|
||||
|
||||
if (!collection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data.posts) {
|
||||
await this.addPosts(collection, data.posts);
|
||||
}
|
||||
|
||||
Object.assign(collection, data);
|
||||
await this.repository.save(collection);
|
||||
await this.collectionsRepository.save(collection);
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<Collection | null> {
|
||||
return await this.repository.getById(id);
|
||||
return await this.collectionsRepository.getById(id);
|
||||
}
|
||||
|
||||
async getAll(options?: any): Promise<{data: Collection[], meta: any}> {
|
||||
const collections = await this.repository.getAll(options);
|
||||
const collections = await this.collectionsRepository.getAll(options);
|
||||
|
||||
return {
|
||||
data: collections,
|
||||
|
21
ghost/collections/src/PostDTO.ts
Normal file
21
ghost/collections/src/PostDTO.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export class PostDTO {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
featured: boolean;
|
||||
publishedAt: Date;
|
||||
deleted: boolean;
|
||||
|
||||
constructor(data: any) {
|
||||
this.id = data.id;
|
||||
this.title = data.title;
|
||||
this.slug = data.slug;
|
||||
this.featured = data.featured;
|
||||
this.publishedAt = data.published_at;
|
||||
this.deleted = data.deleted;
|
||||
}
|
||||
|
||||
static async map(data: any): Promise<PostDTO> {
|
||||
return new PostDTO(data);
|
||||
}
|
||||
}
|
17
ghost/collections/src/PostsDataRepositoryBookshelf.ts
Normal file
17
ghost/collections/src/PostsDataRepositoryBookshelf.ts
Normal file
@ -0,0 +1,17 @@
|
||||
type PostsDataRepositoryBookshelfDeps = {
|
||||
Post: any;
|
||||
}
|
||||
|
||||
export class PostsDataRepositoryBookshelf {
|
||||
Post;
|
||||
|
||||
constructor(deps: PostsDataRepositoryBookshelfDeps) {
|
||||
this.Post = deps.Post;
|
||||
}
|
||||
|
||||
async getBulk(ids: string[]) {
|
||||
return await this.Post.fetchAll({
|
||||
filter: `id:[${ids.join(',')}]`
|
||||
});
|
||||
}
|
||||
}
|
22
ghost/collections/src/PostsDataRepositoryInMemory.ts
Normal file
22
ghost/collections/src/PostsDataRepositoryInMemory.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import {InMemoryRepository} from '@tryghost/in-memory-repository';
|
||||
import {PostDTO} from './PostDTO';
|
||||
|
||||
export class PostsDataRepositoryInMemory extends InMemoryRepository<string, PostDTO> {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
getBulk(ids: string[]): Promise<PostDTO[]> {
|
||||
const postDTOs = this.getAll({
|
||||
filter: `id:[${ids.join(',')}]`
|
||||
});
|
||||
|
||||
return postDTOs;
|
||||
}
|
||||
|
||||
protected toPrimitive(entity: PostDTO): object {
|
||||
return {
|
||||
...entity
|
||||
};
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
export * from './CollectionsService';
|
||||
export * from './CollectionsRepositoryInMemory';
|
||||
export * from './PostsDataRepositoryInMemory';
|
||||
export * from './PostsDataRepositoryBookshelf';
|
||||
export * from './Collection';
|
||||
|
@ -20,7 +20,12 @@ describe('Collection', function () {
|
||||
|
||||
it('Can serialize Collection to JSON', async function () {
|
||||
const collection = await Collection.create({
|
||||
title: 'Serialize me'
|
||||
title: 'Serialize me',
|
||||
posts: [{
|
||||
id: 'post-1'
|
||||
}, {
|
||||
id: 'post-2'
|
||||
}]
|
||||
});
|
||||
|
||||
const json = collection.toJSON();
|
||||
@ -30,7 +35,7 @@ describe('Collection', function () {
|
||||
assert.equal(json.title, 'Serialize me');
|
||||
assert.ok(collection.createdAt instanceof Date);
|
||||
assert.ok(collection.updatedAt instanceof Date);
|
||||
assert.equal(Object.keys(json).length, 9, 'should only have 9 keys');
|
||||
assert.equal(Object.keys(json).length, 10, 'should only have 9 keys + 1 posts relation');
|
||||
assert.deepEqual(Object.keys(json), [
|
||||
'id',
|
||||
'title',
|
||||
@ -40,7 +45,17 @@ describe('Collection', function () {
|
||||
'filter',
|
||||
'featureImage',
|
||||
'createdAt',
|
||||
'updatedAt'
|
||||
'updatedAt',
|
||||
'posts'
|
||||
]);
|
||||
|
||||
assert.equal(json.posts.length, 2, 'should have 2 posts');
|
||||
const serializedPost = json.posts[0];
|
||||
assert.equal(Object.keys(serializedPost).length, 3, 'should only have 3 keys');
|
||||
assert.deepEqual(Object.keys(serializedPost), [
|
||||
'id',
|
||||
'title',
|
||||
'slug'
|
||||
]);
|
||||
});
|
||||
|
||||
|
23
ghost/collections/test/PostsDataRepositoryBookshelf.test.ts
Normal file
23
ghost/collections/test/PostsDataRepositoryBookshelf.test.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import sinon from 'sinon';
|
||||
import assert from 'assert';
|
||||
import {PostsDataRepositoryBookshelf} from '../src/PostsDataRepositoryBookshelf';
|
||||
|
||||
describe('PostsDataRepositoryBookshelf', function () {
|
||||
let Post: any;
|
||||
|
||||
beforeEach(async function () {
|
||||
Post = {
|
||||
fetchAll: sinon.stub().resolves([])
|
||||
};
|
||||
});
|
||||
|
||||
it('Can fetch posts by ids', async function () {
|
||||
const repository = new PostsDataRepositoryBookshelf({
|
||||
Post: Post
|
||||
});
|
||||
|
||||
await repository.getBulk(['1', '2']);
|
||||
|
||||
assert.ok(Post.fetchAll.calledOnce);
|
||||
});
|
||||
});
|
@ -1,13 +1,32 @@
|
||||
import assert from 'assert';
|
||||
import {CollectionsService} from '../src/index';
|
||||
import {CollectionsRepositoryInMemory} from '../src/CollectionsRepositoryInMemory';
|
||||
import {CollectionsService, CollectionsRepositoryInMemory, PostsDataRepositoryInMemory} from '../src/index';
|
||||
import {PostDTO} from '../src/PostDTO';
|
||||
|
||||
describe('collections', function () {
|
||||
import {posts as postFixtures} from './fixtures/posts';
|
||||
|
||||
const buildPostsRepositoryWithFixtures = async (): Promise<PostsDataRepositoryInMemory> => {
|
||||
const repository = new PostsDataRepositoryInMemory();
|
||||
|
||||
for (const post of postFixtures) {
|
||||
const postDTO = await PostDTO.map(post);
|
||||
await repository.save(postDTO);
|
||||
}
|
||||
|
||||
return repository;
|
||||
};
|
||||
|
||||
describe('CollectionsService', function () {
|
||||
let collectionsService: CollectionsService;
|
||||
let postsRepository: PostsDataRepositoryInMemory;
|
||||
|
||||
beforeEach(function () {
|
||||
const repository = new CollectionsRepositoryInMemory();
|
||||
collectionsService = new CollectionsService({repository});
|
||||
beforeEach(async function () {
|
||||
const collectionsRepository = new CollectionsRepositoryInMemory();
|
||||
postsRepository = await buildPostsRepositoryWithFixtures();
|
||||
|
||||
collectionsService = new CollectionsService({
|
||||
collectionsRepository,
|
||||
postsRepository
|
||||
});
|
||||
});
|
||||
|
||||
it('Instantiates a CollectionsService', function () {
|
||||
@ -63,5 +82,26 @@ describe('collections', function () {
|
||||
|
||||
assert.equal(editedCollection, null, 'Collection should be null');
|
||||
});
|
||||
|
||||
it('Adds a Post to a Collection', async function () {
|
||||
const collection = await collectionsService.save({
|
||||
title: 'testing collections',
|
||||
description: 'testing collections description',
|
||||
type: 'manual',
|
||||
deleted: false
|
||||
});
|
||||
|
||||
const posts = await postsRepository.getAll();
|
||||
|
||||
const editedCollection = await collectionsService.edit({
|
||||
id: collection.id,
|
||||
posts: [{
|
||||
id: posts[0].id
|
||||
}]
|
||||
});
|
||||
|
||||
assert.equal(editedCollection?.posts.length, 1, 'Collection should have one post');
|
||||
assert.equal(editedCollection?.posts[0].id, posts[0].id, 'Collection should have the correct post');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
19
ghost/collections/test/fixtures/posts.ts
vendored
Normal file
19
ghost/collections/test/fixtures/posts.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
export const posts = [{
|
||||
id: 'post-1',
|
||||
title: 'Post 1',
|
||||
slug: 'post-1',
|
||||
featured: false,
|
||||
published_at: new Date('2023-03-15T07:19:07.447Z')
|
||||
}, {
|
||||
id: 'post-2',
|
||||
title: 'Post 2',
|
||||
slug: 'post-2',
|
||||
featured: false,
|
||||
published_at: new Date('2023-04-05T07:20:07.447Z')
|
||||
}, {
|
||||
id: 'post-3',
|
||||
title: 'Post 3',
|
||||
slug: 'post-3',
|
||||
featured: true,
|
||||
published_at: new Date('2023-05-25T07:21:07.447Z')
|
||||
}];
|
Loading…
Reference in New Issue
Block a user