From 4fe9e5fac0d8285de13240261952346cabcfff17 Mon Sep 17 00:00:00 2001 From: Naz Date: Thu, 25 May 2023 22:39:43 +0700 Subject: [PATCH] Added posts editing to collections in Admin API refs https://github.com/TryGhost/Team/issues/3260 - Allows to manually manage posts assigned to collections through Collections Admin API --- .../serializers/output/mappers/collections.js | 7 +- .../core/server/services/collections/index.js | 16 ++- .../__snapshots__/collections.test.js.snap | 101 ++++++++++++++++-- .../test/e2e-api/admin/collections.test.js | 57 ++++++++++ 4 files changed, 171 insertions(+), 10 deletions(-) diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/collections.js b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/collections.js index 0b54be19f7..0751061ff7 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/collections.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/collections.js @@ -16,7 +16,12 @@ const mapper = (collection) => { filter: json.filter, feature_image: json.featureImage, created_at: json.createdAt.toISOString().replace(/\d{3}Z$/, '000Z'), - updated_at: json.updatedAt.toISOString().replace(/\d{3}Z$/, '000Z') + updated_at: json.updatedAt.toISOString().replace(/\d{3}Z$/, '000Z'), + posts: json.posts.map(post => ({ + id: post.id, + title: post.title, + slug: post.slug + })) }; return serialized; diff --git a/ghost/core/core/server/services/collections/index.js b/ghost/core/core/server/services/collections/index.js index 049b5e4990..59b1a37008 100644 --- a/ghost/core/core/server/services/collections/index.js +++ b/ghost/core/core/server/services/collections/index.js @@ -1,12 +1,22 @@ -const {CollectionsService, CollectionsRepositoryInMemory} = require('@tryghost/collections'); +const models = require('../../models'); +const { + CollectionsService, + CollectionsRepositoryInMemory, + PostsDataRepositoryBookshelf +} = require('@tryghost/collections'); class CollectionsServiceWrapper { api; constructor() { - const inMemoryCollectionsRepository = new CollectionsRepositoryInMemory(); + const collectionsRepositoryInMemory = new CollectionsRepositoryInMemory(); + const postsDataRepositoryBookshelf = new PostsDataRepositoryBookshelf({ + Post: models.Post + }); + const collectionsService = new CollectionsService({ - repository: inMemoryCollectionsRepository + collectionsRepository: collectionsRepositoryInMemory, + postsRepository: postsDataRepositoryBookshelf }); this.api = { diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/collections.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/collections.test.js.snap index be5f1fedf7..598fa6c886 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/collections.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/collections.test.js.snap @@ -9,6 +9,7 @@ Object { "feature_image": null, "filter": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "posts": Array [], "title": "Test Collection", "type": "manual", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -21,7 +22,7 @@ exports[`Collections API Can add a Collection 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": "252", + "content-length": "263", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -41,6 +42,7 @@ Object { "feature_image": null, "filter": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "posts": Array [], "title": "Test Collection", "type": "manual", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -63,7 +65,7 @@ exports[`Collections API Can browse Collections 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": "339", + "content-length": "350", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -81,6 +83,7 @@ Object { "feature_image": null, "filter": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "posts": Array [], "title": "Test Collection to Delete", "type": "manual", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -93,7 +96,7 @@ exports[`Collections API Can delete a Collection 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": "237", + "content-length": "248", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -221,6 +224,7 @@ Object { "feature_image": null, "filter": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "posts": Array [], "title": "Test Collection to Read", "type": "manual", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -233,7 +237,7 @@ exports[`Collections API Can read a Collection 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": "235", + "content-length": "246", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -253,6 +257,7 @@ Object { "feature_image": null, "filter": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "posts": Array [], "title": "Test Collection to Read", "type": "manual", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -265,7 +270,7 @@ exports[`Collections API Can read a Collection 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": "235", + "content-length": "246", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -305,6 +310,89 @@ Object { } `; +exports[`Collections API edit Can add a Post to a Collection 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": null, + "feature_image": null, + "filter": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "posts": Array [ + Object { + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + }, + Object { + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + }, + Object { + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + }, + ], + "title": "Test Collection Edited", + "type": "manual", + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + }, + ], +} +`; + +exports[`Collections API edit Can add a Post to a Collection 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": "346", + "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-cache-invalidate": "/*", + "x-powered-by": "Express", +} +`; + +exports[`Collections API edit Can add a Post to a Collection 3: [body] 1`] = ` +Object { + "collections": Array [ + Object { + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "description": null, + "feature_image": null, + "filter": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "posts": Array [ + Object { + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + }, + Object { + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + }, + Object { + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + }, + ], + "title": "Test Collection Edited", + "type": "manual", + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + }, + ], +} +`; + +exports[`Collections API edit Can add a Post to a Collection 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": "346", + "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 edit Can edit a Collection 1: [body] 1`] = ` Object { "collections": Array [ @@ -314,6 +402,7 @@ Object { "feature_image": null, "filter": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "posts": Array [], "title": "Test Collection Edited", "type": "manual", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -326,7 +415,7 @@ exports[`Collections API edit Can edit a Collection 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": "234", + "content-length": "245", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/admin/collections.test.js b/ghost/core/test/e2e-api/admin/collections.test.js index 485f3eeb67..f299d2f41f 100644 --- a/ghost/core/test/e2e-api/admin/collections.test.js +++ b/ghost/core/test/e2e-api/admin/collections.test.js @@ -20,6 +20,19 @@ const matchCollection = { updated_at: anyISODateTime }; +/** + * + * @param {number} postCount + */ +const buildMatcher = (postCount) => { + return { + ...matchCollection, + posts: Array(postCount).fill({ + id: anyObjectId + }) + }; +}; + describe('Collections API', function () { let agent; @@ -163,6 +176,50 @@ describe('Collections API', function () { etag: anyEtag }); }); + + it('Can add a Post to a Collection', async function () { + const postsToAttach = [{ + id: fixtureManager.get('posts', 0).id + }, { + id: fixtureManager.get('posts', 2).id + }, { + id: fixtureManager.get('posts', 3).id + }]; + + const collectionId = collectionToEdit.id; + + const editResponse = await agent + .put(`/collections/${collectionId}/`) + .body({ + collections: [{ + posts: postsToAttach + }] + }) + .expectStatus(200) + .matchHeaderSnapshot({ + 'content-version': anyContentVersion, + etag: anyEtag + }) + .matchBodySnapshot({ + collections: [buildMatcher(3)] + }); + + assert.equal(editResponse.body.collections[0].posts.length, 3, 'Posts should have been added to a Collection'); + + // verify the posts are persisted across requests + const readResponse = await agent + .get(`/collections/${collectionId}/`) + .expectStatus(200) + .matchHeaderSnapshot({ + 'content-version': anyContentVersion, + etag: anyEtag + }) + .matchBodySnapshot({ + collections: [buildMatcher(3)] + }); + + assert.equal(readResponse.body.collections[0].posts.length, 3, 'Posts should have been added to a Collection'); + }); }); it('Can delete a Collection', async function () {