Added support for filtering snippets to mobiledoc/lexical (#16636)

refs TryGhost/Team#2904


<!-- Leave the line below if you'd like GitHub Copilot to generate a
summary from your commit -->
<!--
copilot:summary
-->
### <samp>🤖 Generated by Copilot at b3f5423</samp>

This pull request adds support for multiple formats of snippet content,
especially the `lexical` format, to the Ghost CMS. It modifies the
snippets API, model, and test files to handle the format conversion,
filtering, and serialization of snippets.
This commit is contained in:
Elena Baidakova 2023-04-17 10:54:08 +04:00 committed by GitHub
parent 5d43101f40
commit 7f184d2451
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 297 additions and 7 deletions

View File

@ -15,9 +15,18 @@ module.exports = {
options: [
'limit',
'order',
'page'
'page',
'formats',
'filter'
],
permissions: true,
validation: {
options: {
formats: {
values: models.Snippet.allowedFormats
}
}
},
query(frame) {
return models.Snippet.findPage(frame.options);
}
@ -25,6 +34,9 @@ module.exports = {
read: {
headers: {},
options: [
'formats'
],
data: [
'id'
],
@ -46,6 +58,9 @@ module.exports = {
add: {
statusCode: 201,
headers: {},
options: [
'formats'
],
permissions: true,
query(frame) {
return models.Snippet.add(frame.data.snippets[0], frame.options)
@ -62,7 +77,8 @@ module.exports = {
edit: {
headers: {},
options: [
'id'
'id',
'formats'
],
validation: {
options: {

View File

@ -12,6 +12,7 @@ module.exports = (snippet, frame) => {
name: json.name,
// @ts-ignore
mobiledoc: json.mobiledoc,
lexical: json.lexical,
created_at: json.created_at,
updated_at: json.updated_at,
created_by: json.created_by,
@ -24,6 +25,7 @@ module.exports = (snippet, frame) => {
* @prop {string} id
* @prop {string} [name]
* @prop {string} [mobiledoc]
* @prop {string} [lexical]
* @prop {string} created_at
* @prop {string} updated_at
* @prop {string} created_by

View File

@ -1,6 +1,8 @@
const ghostBookshelf = require('./base');
const urlUtils = require('../../shared/url-utils');
const mobiledocLib = require('../lib/mobiledoc');
const lexicalLib = require('../lib/lexical');
const _ = require('lodash');
const Snippet = ghostBookshelf.Model.extend({
tableName: 'snippets',
@ -10,6 +12,13 @@ const Snippet = ghostBookshelf.Model.extend({
attrs.mobiledoc = urlUtils.mobiledocToTransformReady(attrs.mobiledoc, {cardTransformers: mobiledocLib.cards});
}
if (attrs.lexical) {
attrs.lexical = urlUtils.lexicalToTransformReady(attrs.lexical, {
nodes: lexicalLib.nodes,
transformMap: lexicalLib.urlTransformMap
});
}
return attrs;
},
@ -20,8 +29,35 @@ const Snippet = ghostBookshelf.Model.extend({
attrs.mobiledoc = urlUtils.transformReadyToAbsolute(attrs.mobiledoc);
}
if (attrs.lexical) {
attrs.lexical = urlUtils.transformReadyToAbsolute(attrs.lexical);
}
return attrs;
},
formatsToJSON: function formatsToJSON(attrs, options) {
const defaultFormats = ['mobiledoc'];
const formatsToKeep = options.formats || defaultFormats;
// Iterate over all known formats, and if they are not in the keep list, remove them
_.each(Snippet.allowedFormats, function (format) {
if (formatsToKeep.indexOf(format) === -1) {
delete attrs[format];
}
});
return attrs;
},
toJSON: function toJSON(options) {
let attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
attrs = this.formatsToJSON(attrs, options);
return attrs;
}
}, {
allowedFormats: ['mobiledoc', 'lexical']
});
const Snippets = ghostBookshelf.Collection.extend({

View File

@ -62,7 +62,7 @@
"@tryghost/adapter-base-cache": "0.1.5",
"@tryghost/adapter-cache-redis": "0.0.0",
"@tryghost/adapter-manager": "0.0.0",
"@tryghost/admin-api-schema": "4.2.3",
"@tryghost/admin-api-schema": "4.3.0",
"@tryghost/api-framework": "0.0.0",
"@tryghost/api-version-compatibility-service": "0.0.0",
"@tryghost/audience-feedback": "0.0.0",

View File

@ -28,6 +28,34 @@ Object {
}
`;
exports[`Snippets API Can add lexical 1: [body] 1`] = `
Object {
"snippets": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"lexical": "{\\"node\\":\\"text\\"}",
"name": "test lexical",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
},
],
}
`;
exports[`Snippets API Can add lexical 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": "182",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/snippets\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Snippets API Can browse 1: [body] 1`] = `
Object {
"meta": Object {
@ -72,6 +100,43 @@ Object {
}
`;
exports[`Snippets API Can browse lexical 1: [body] 1`] = `
Object {
"meta": Object {
"pagination": Object {
"limit": 15,
"next": null,
"page": 1,
"pages": 1,
"prev": null,
"total": 1,
},
},
"snippets": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"lexical": "{\\"node\\":\\"text\\"}",
"name": "test lexical",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
},
],
}
`;
exports[`Snippets API Can browse lexical 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": "270",
"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[`Snippets API Can destroy 1: [body] 1`] = `
Object {
"snippets": Array [
@ -197,6 +262,61 @@ Object {
}
`;
exports[`Snippets API Can edit lexical 1: [body] 1`] = `
Object {
"snippets": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"lexical": "{}",
"name": "change me",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
},
],
}
`;
exports[`Snippets API Can edit lexical 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": "162",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/snippets\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Snippets API Can edit lexical 3: [body] 1`] = `
Object {
"snippets": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"lexical": "{\\"node\\":\\"text\\"}",
"name": "changed lexical",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
},
],
}
`;
exports[`Snippets API Can edit lexical 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": "185",
"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[`Snippets API Can read 1: [body] 1`] = `
Object {
"snippets": Array [
@ -224,6 +344,33 @@ Object {
}
`;
exports[`Snippets API Can read lexical 1: [body] 1`] = `
Object {
"snippets": Array [
Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"lexical": null,
"name": "Test snippet 1",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
},
],
}
`;
exports[`Snippets API Can read lexical 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": "167",
"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[`Snippets API Cannot destroy non-existent snippet 1: [body] 1`] = `
Object {
"errors": Array [

View File

@ -62,6 +62,19 @@ describe('Snippets API', function () {
});
});
it('Can read lexical', async function () {
await agent
.get(`snippets/${fixtureManager.get('snippets', 0).id}/?formats=lexical`)
.expectStatus(200)
.matchBodySnapshot({
snippets: [matchSnippet]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Can edit', async function () {
const snippetToChange = {
name: 'change me',
@ -159,4 +172,79 @@ describe('Snippets API', function () {
etag: anyEtag
});
});
it('Can add lexical', async function () {
const snippet = {
name: 'test lexical',
lexical: JSON.stringify({node: 'text'}),
mobiledoc: '{}'
};
await agent
.post('snippets/?formats=lexical')
.body({snippets: [snippet]})
.expectStatus(201)
.matchBodySnapshot({
snippets: [matchSnippet]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag,
location: anyLocationFor('snippets')
});
});
it('Can browse lexical', async function () {
await agent
.get('snippets?formats=lexical&filter=lexical:-null')
.expectStatus(200)
.matchBodySnapshot({
snippets: new Array(1).fill(matchSnippet)
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
it('Can edit lexical', async function () {
const snippetToChange = {
name: 'change me',
mobiledoc: '{}',
lexical: '{}'
};
const snippetChanged = {
name: 'changed lexical',
mobiledoc: '{}',
lexical: JSON.stringify({node: 'text'})
};
const {body} = await agent
.post(`snippets/?formats=lexical`)
.body({snippets: [snippetToChange]})
.expectStatus(201)
.matchBodySnapshot({
snippets: [matchSnippet]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag,
location: anyLocationFor('snippets')
});
const newsnippet = body.snippets[0];
await agent
.put(`snippets/${newsnippet.id}/?formats=lexical`)
.body({snippets: [snippetChanged]})
.expectStatus(200)
.matchBodySnapshot({
snippets: [matchSnippet]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});
});

View File

@ -177,6 +177,7 @@ describe('Unit: utils/serializers/output/mappers', function () {
id: snippet.id,
name: snippet.name,
mobiledoc: snippet.mobiledoc,
lexical: snippet.lexical,
created_at: snippet.created_at,
updated_at: snippet.updated_at,
created_by: snippet.created_by,

View File

@ -5557,10 +5557,10 @@
resolved "https://registry.yarnpkg.com/@tryghost/adapter-base-cache/-/adapter-base-cache-0.1.5.tgz#66021c4e3e92bc623c82728ab50ca497ac41f7ae"
integrity sha512-ZAG7Qzn0RioU6yde67T9cneRtoD1ZxGcjwH81DdbL8ZqiPi66bmPw5TVOiEiFnny2XSDsPuUgc4/PxFO/RfV0g==
"@tryghost/admin-api-schema@4.2.3":
version "4.2.3"
resolved "https://registry.yarnpkg.com/@tryghost/admin-api-schema/-/admin-api-schema-4.2.3.tgz#fb6216f0c6ed103a32925d4ab2fa7582bb4a000d"
integrity sha512-3UmabEJd+fUZv3cSkpQ9Zo17DZpDVuVvG+9Mh0H+zZBmsw7FuTAlb5KzG4T3pFVv6X2/xrf5PMQBfTt+jGR6+g==
"@tryghost/admin-api-schema@4.3.0":
version "4.3.0"
resolved "https://registry.yarnpkg.com/@tryghost/admin-api-schema/-/admin-api-schema-4.3.0.tgz#f3ecc16d9ab7e2a1c25e600f4663738e5e6a63db"
integrity sha512-LyS3OnpRjuM89JNGxJAH3t9eFGPVS1CGEpDREHnD6SNn6J9iyYH3hmS8KOfZ87RXmwhF5qOODgaapZ/uMyRMtg==
dependencies:
"@tryghost/errors" "^1.0.0"
ajv "^6.12.6"