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:
Naz 2023-05-25 22:34:59 +07:00 committed by naz
parent c7954fa695
commit 66d489b8b3
10 changed files with 212 additions and 19 deletions

View File

@ -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 || []
});
}
}

View File

@ -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,

View 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);
}
}

View 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(',')}]`
});
}
}

View 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
};
}
}

View File

@ -1,3 +1,5 @@
export * from './CollectionsService';
export * from './CollectionsRepositoryInMemory';
export * from './PostsDataRepositoryInMemory';
export * from './PostsDataRepositoryBookshelf';
export * from './Collection';

View File

@ -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'
]);
});

View 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);
});
});

View File

@ -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');
});
});
});

View 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')
}];