diff --git a/ghost/collections/src/Collection.ts b/ghost/collections/src/Collection.ts index 567be018c5..baa29848cd 100644 --- a/ghost/collections/src/Collection.ts +++ b/ghost/collections/src/Collection.ts @@ -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 || [] }); } } diff --git a/ghost/collections/src/CollectionsService.ts b/ghost/collections/src/CollectionsService.ts index f04c57003e..38b0e96555 100644 --- a/ghost/collections/src/CollectionsService.ts +++ b/ghost/collections/src/CollectionsService.ts @@ -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 { 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 { + 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 { - 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 { - 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, diff --git a/ghost/collections/src/PostDTO.ts b/ghost/collections/src/PostDTO.ts new file mode 100644 index 0000000000..b2740835ea --- /dev/null +++ b/ghost/collections/src/PostDTO.ts @@ -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 { + return new PostDTO(data); + } +} diff --git a/ghost/collections/src/PostsDataRepositoryBookshelf.ts b/ghost/collections/src/PostsDataRepositoryBookshelf.ts new file mode 100644 index 0000000000..79ea75c898 --- /dev/null +++ b/ghost/collections/src/PostsDataRepositoryBookshelf.ts @@ -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(',')}]` + }); + } +} diff --git a/ghost/collections/src/PostsDataRepositoryInMemory.ts b/ghost/collections/src/PostsDataRepositoryInMemory.ts new file mode 100644 index 0000000000..90aa925e68 --- /dev/null +++ b/ghost/collections/src/PostsDataRepositoryInMemory.ts @@ -0,0 +1,22 @@ +import {InMemoryRepository} from '@tryghost/in-memory-repository'; +import {PostDTO} from './PostDTO'; + +export class PostsDataRepositoryInMemory extends InMemoryRepository { + constructor() { + super(); + } + + getBulk(ids: string[]): Promise { + const postDTOs = this.getAll({ + filter: `id:[${ids.join(',')}]` + }); + + return postDTOs; + } + + protected toPrimitive(entity: PostDTO): object { + return { + ...entity + }; + } +} diff --git a/ghost/collections/src/index.ts b/ghost/collections/src/index.ts index 626ea081dc..29d4f0afd5 100644 --- a/ghost/collections/src/index.ts +++ b/ghost/collections/src/index.ts @@ -1,3 +1,5 @@ export * from './CollectionsService'; export * from './CollectionsRepositoryInMemory'; +export * from './PostsDataRepositoryInMemory'; +export * from './PostsDataRepositoryBookshelf'; export * from './Collection'; diff --git a/ghost/collections/test/Collection.test.ts b/ghost/collections/test/Collection.test.ts index ae936391cd..304d7b18b3 100644 --- a/ghost/collections/test/Collection.test.ts +++ b/ghost/collections/test/Collection.test.ts @@ -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' ]); }); diff --git a/ghost/collections/test/PostsDataRepositoryBookshelf.test.ts b/ghost/collections/test/PostsDataRepositoryBookshelf.test.ts new file mode 100644 index 0000000000..36e356896e --- /dev/null +++ b/ghost/collections/test/PostsDataRepositoryBookshelf.test.ts @@ -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); + }); +}); diff --git a/ghost/collections/test/collections.test.ts b/ghost/collections/test/collections.test.ts index 8d9656ba28..cc23febd05 100644 --- a/ghost/collections/test/collections.test.ts +++ b/ghost/collections/test/collections.test.ts @@ -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 => { + 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'); + }); }); }); diff --git a/ghost/collections/test/fixtures/posts.ts b/ghost/collections/test/fixtures/posts.ts new file mode 100644 index 0000000000..915964800c --- /dev/null +++ b/ghost/collections/test/fixtures/posts.ts @@ -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') +}];