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
This commit is contained in:
Naz 2023-05-25 22:39:43 +07:00 committed by naz
parent 66d489b8b3
commit 4fe9e5fac0
4 changed files with 171 additions and 10 deletions

View File

@ -16,7 +16,12 @@ const mapper = (collection) => {
filter: json.filter, filter: json.filter,
feature_image: json.featureImage, feature_image: json.featureImage,
created_at: json.createdAt.toISOString().replace(/\d{3}Z$/, '000Z'), 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; return serialized;

View File

@ -1,12 +1,22 @@
const {CollectionsService, CollectionsRepositoryInMemory} = require('@tryghost/collections'); const models = require('../../models');
const {
CollectionsService,
CollectionsRepositoryInMemory,
PostsDataRepositoryBookshelf
} = require('@tryghost/collections');
class CollectionsServiceWrapper { class CollectionsServiceWrapper {
api; api;
constructor() { constructor() {
const inMemoryCollectionsRepository = new CollectionsRepositoryInMemory(); const collectionsRepositoryInMemory = new CollectionsRepositoryInMemory();
const postsDataRepositoryBookshelf = new PostsDataRepositoryBookshelf({
Post: models.Post
});
const collectionsService = new CollectionsService({ const collectionsService = new CollectionsService({
repository: inMemoryCollectionsRepository collectionsRepository: collectionsRepositoryInMemory,
postsRepository: postsDataRepositoryBookshelf
}); });
this.api = { this.api = {

View File

@ -9,6 +9,7 @@ Object {
"feature_image": null, "feature_image": null,
"filter": null, "filter": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"posts": Array [],
"title": "Test Collection", "title": "Test Collection",
"type": "manual", "type": "manual",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"access-control-allow-origin": "http://127.0.0.1:2369", "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", "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-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -41,6 +42,7 @@ Object {
"feature_image": null, "feature_image": null,
"filter": null, "filter": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"posts": Array [],
"title": "Test Collection", "title": "Test Collection",
"type": "manual", "type": "manual",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"access-control-allow-origin": "http://127.0.0.1:2369", "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", "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-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -81,6 +83,7 @@ Object {
"feature_image": null, "feature_image": null,
"filter": null, "filter": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"posts": Array [],
"title": "Test Collection to Delete", "title": "Test Collection to Delete",
"type": "manual", "type": "manual",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"access-control-allow-origin": "http://127.0.0.1:2369", "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", "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-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -221,6 +224,7 @@ Object {
"feature_image": null, "feature_image": null,
"filter": null, "filter": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"posts": Array [],
"title": "Test Collection to Read", "title": "Test Collection to Read",
"type": "manual", "type": "manual",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"access-control-allow-origin": "http://127.0.0.1:2369", "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", "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-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -253,6 +257,7 @@ Object {
"feature_image": null, "feature_image": null,
"filter": null, "filter": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"posts": Array [],
"title": "Test Collection to Read", "title": "Test Collection to Read",
"type": "manual", "type": "manual",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"access-control-allow-origin": "http://127.0.0.1:2369", "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", "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-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "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`] = ` exports[`Collections API edit Can edit a Collection 1: [body] 1`] = `
Object { Object {
"collections": Array [ "collections": Array [
@ -314,6 +402,7 @@ Object {
"feature_image": null, "feature_image": null,
"filter": null, "filter": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"posts": Array [],
"title": "Test Collection Edited", "title": "Test Collection Edited",
"type": "manual", "type": "manual",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "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 { Object {
"access-control-allow-origin": "http://127.0.0.1:2369", "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", "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-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,

View File

@ -20,6 +20,19 @@ const matchCollection = {
updated_at: anyISODateTime updated_at: anyISODateTime
}; };
/**
*
* @param {number} postCount
*/
const buildMatcher = (postCount) => {
return {
...matchCollection,
posts: Array(postCount).fill({
id: anyObjectId
})
};
};
describe('Collections API', function () { describe('Collections API', function () {
let agent; let agent;
@ -163,6 +176,50 @@ describe('Collections API', function () {
etag: anyEtag 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 () { it('Can delete a Collection', async function () {