Optimized usage of urls in API v2

refs #9866

- Extracted url decoration logic to utility in output serializers in posts, pages, users, and tags
- Added test cases for url usage by child object (tags of posts)
This commit is contained in:
Nazar Gargol 2018-10-15 19:44:00 +02:00
parent 2fbc5aa257
commit d582c06eee
7 changed files with 129 additions and 181 deletions

View File

@ -1,76 +1,5 @@
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:pages');
const urlService = require('../../../../../services/url');
// @TODO: refactor if we add users+tags controllers
const urlsForUser = (user) => {
user.url = urlService.getUrlByResourceId(user.id, {absolute: true});
if (user.profile_image) {
user.profile_image = urlService.utils.urlFor('image', {image: user.profile_image}, true);
}
if (user.cover_image) {
user.cover_image = urlService.utils.urlFor('image', {image: user.cover_image}, true);
}
return user;
};
const urlsForTag = (tag) => {
tag.url = urlService.getUrlByResourceId(tag.id, {absolute: true});
if (tag.feature_image) {
tag.feature_image = urlService.utils.urlFor('image', {image: tag.feature_image}, true);
}
return tag;
};
// @TODO: Update the url decoration in https://github.com/TryGhost/Ghost/pull/9969.
const absoluteUrls = (attrs, options) => {
attrs.url = urlService.getUrlByResourceId(attrs.id, {absolute: true});
if (attrs.feature_image) {
attrs.feature_image = urlService.utils.urlFor('image', {image: attrs.feature_image}, true);
}
if (attrs.og_image) {
attrs.og_image = urlService.utils.urlFor('image', {image: attrs.og_image}, true);
}
if (attrs.twitter_image) {
attrs.twitter_image = urlService.utils.urlFor('image', {image: attrs.twitter_image}, true);
}
if (attrs.html) {
attrs.html = urlService.utils.makeAbsoluteUrls(attrs.html, urlService.utils.urlFor('home', true), attrs.url).html();
}
if (options.columns && !options.columns.includes('url')) {
delete attrs.url;
}
if (options && options.withRelated) {
options.withRelated.forEach((relation) => {
// @NOTE: this block also decorates primary_tag/primary_author objects as they
// are being passed by reference in tags/authors. Might be refactored into more explicit call
// in the future, but is good enough for current use-case
if (relation === 'tags' && attrs.tags) {
attrs.tags = attrs.tags.map(tag => urlsForTag(tag));
}
if (relation === 'author' && attrs.author) {
attrs.author = urlsForUser(attrs.author);
}
if (relation === 'authors' && attrs.authors) {
attrs.authors = attrs.authors.map(author => urlsForUser(author));
}
});
}
return attrs;
};
const url = require('./utils/url');
module.exports = {
all(models, apiConfig, frame) {
@ -78,7 +7,7 @@ module.exports = {
if (models.meta) {
frame.response = {
pages: models.data.map(model => absoluteUrls(model.toJSON(frame.options), frame.options)),
pages: models.data.map(model => url.forPost(model.id, model.toJSON(frame.options), frame.options)),
meta: models.meta
};
@ -86,7 +15,7 @@ module.exports = {
}
frame.response = {
pages: [absoluteUrls(models.toJSON(frame.options), frame.options)]
pages: [url.forPost(models.id, models.toJSON(frame.options), frame.options)]
};
debug(frame.response);

View File

@ -1,76 +1,5 @@
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:posts');
const urlService = require('../../../../../services/url');
// @TODO: refactor if we add users+tags controllers
const urlsForUser = (user) => {
user.url = urlService.getUrlByResourceId(user.id, {absolute: true});
if (user.profile_image) {
user.profile_image = urlService.utils.urlFor('image', {image: user.profile_image}, true);
}
if (user.cover_image) {
user.cover_image = urlService.utils.urlFor('image', {image: user.cover_image}, true);
}
return user;
};
const urlsForTag = (tag) => {
tag.url = urlService.getUrlByResourceId(tag.id, {absolute: true});
if (tag.feature_image) {
tag.feature_image = urlService.utils.urlFor('image', {image: tag.feature_image}, true);
}
return tag;
};
// @TODO: Update the url decoration in https://github.com/TryGhost/Ghost/pull/9969.
const absoluteUrls = (attrs, options) => {
attrs.url = urlService.getUrlByResourceId(attrs.id, {absolute: true});
if (attrs.feature_image) {
attrs.feature_image = urlService.utils.urlFor('image', {image: attrs.feature_image}, true);
}
if (attrs.og_image) {
attrs.og_image = urlService.utils.urlFor('image', {image: attrs.og_image}, true);
}
if (attrs.twitter_image) {
attrs.twitter_image = urlService.utils.urlFor('image', {image: attrs.twitter_image}, true);
}
if (attrs.html) {
attrs.html = urlService.utils.makeAbsoluteUrls(attrs.html, urlService.utils.urlFor('home', true), attrs.url).html();
}
if (options.columns && !options.columns.includes('url')) {
delete attrs.url;
}
if (options && options.withRelated) {
options.withRelated.forEach((relation) => {
// @NOTE: this block also decorates primary_tag/primary_author objects as they
// are being passed by reference in tags/authors. Might be refactored into more explicit call
// in the future, but is good enough for current use-case
if (relation === 'tags' && attrs.tags) {
attrs.tags = attrs.tags.map(tag => urlsForTag(tag));
}
if (relation === 'author' && attrs.author) {
attrs.author = urlsForUser(attrs.author);
}
if (relation === 'authors' && attrs.authors) {
attrs.authors = attrs.authors.map(author => urlsForUser(author));
}
});
}
return attrs;
};
const url = require('./utils/url');
module.exports = {
all(models, apiConfig, frame) {
@ -83,7 +12,7 @@ module.exports = {
if (models.meta) {
frame.response = {
posts: models.data.map(model => absoluteUrls(model.toJSON(frame.options), frame.options)),
posts: models.data.map(model => url.forPost(model.id, model.toJSON(frame.options), frame.options)),
meta: models.meta
};
@ -92,7 +21,7 @@ module.exports = {
}
frame.response = {
posts: [absoluteUrls(models.toJSON(frame.options), frame.options)]
posts: [url.forPost(models.id, models.toJSON(frame.options), frame.options)]
};
debug(frame.response);

View File

@ -1,15 +1,5 @@
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:tags');
const urlService = require('../../../../../services/url');
const absoluteUrls = (tag) => {
tag.url = urlService.getUrlByResourceId(tag.id, {absolute: true});
if (tag.feature_image) {
tag.feature_image = urlService.utils.urlFor('image', {image: tag.feature_image}, true);
}
return tag;
};
const url = require('./utils/url');
module.exports = {
all(models, apiConfig, frame) {
@ -21,7 +11,7 @@ module.exports = {
if (models.meta) {
frame.response = {
tags: models.data.map(model => absoluteUrls(model.toJSON(frame.options))),
tags: models.data.map(model => url.forTag(model.id, model.toJSON(frame.options), frame.options)),
meta: models.meta
};
@ -29,7 +19,7 @@ module.exports = {
}
frame.response = {
tags: [absoluteUrls(models.toJSON(frame.options))]
tags: [url.forTag(models.id, models.toJSON(frame.options), frame.options)]
};
debug(frame.response);

View File

@ -1,27 +1,13 @@
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:users');
const common = require('../../../../../lib/common');
const urlService = require('../../../../../services/url');
const absoluteUrls = (user) => {
user.url = urlService.getUrlByResourceId(user.id, {absolute: true});
if (user.profile_image) {
user.profile_image = urlService.utils.urlFor('image', {image: user.profile_image}, true);
}
if (user.cover_image) {
user.cover_image = urlService.utils.urlFor('image', {image: user.cover_image}, true);
}
return user;
};
const url = require('./utils/url');
module.exports = {
browse(models, apiConfig, frame) {
debug('browse');
frame.response = {
users: models.data.map(model => absoluteUrls(model.toJSON(frame.options))),
users: models.data.map(model => url.forUser(model.id, model.toJSON(frame.options), frame.options)),
meta: models.meta
};
@ -32,7 +18,7 @@ module.exports = {
debug('read');
frame.response = {
users: [absoluteUrls(model.toJSON(frame.options))]
users: [url.forUser(model.id, model.toJSON(frame.options), frame.options)]
};
debug(frame.response);

View File

@ -0,0 +1,75 @@
const urlService = require('../../../../../../services/url');
const {urlFor, makeAbsoluteUrls} = require('../../../../../../services/url/utils');
const forPost = (id, attrs, options) => {
attrs.url = urlService.getUrlByResourceId(id, {absolute: true});
if (attrs.feature_image) {
attrs.feature_image = urlFor('image', {image: attrs.feature_image}, true);
}
if (attrs.og_image) {
attrs.og_image = urlFor('image', {image: attrs.og_image}, true);
}
if (attrs.twitter_image) {
attrs.twitter_image = urlFor('image', {image: attrs.twitter_image}, true);
}
if (attrs.html) {
attrs.html = makeAbsoluteUrls(attrs.html, urlFor('home', true), attrs.url).html();
}
if (options.columns && !options.columns.includes('url')) {
delete attrs.url;
}
if (options && options.withRelated) {
options.withRelated.forEach((relation) => {
// @NOTE: this block also decorates primary_tag/primary_author objects as they
// are being passed by reference in tags/authors. Might be refactored into more explicit call
// in the future, but is good enough for current use-case
if (relation === 'tags' && attrs.tags) {
attrs.tags = attrs.tags.map(tag => forTag(tag.id, tag));
}
if (relation === 'author' && attrs.author) {
attrs.author = forUser(attrs.author.id, attrs.author, options);
}
if (relation === 'authors' && attrs.authors) {
attrs.authors = attrs.authors.map(author => forUser(author.id, author, options));
}
});
}
return attrs;
};
const forUser = (id, attrs) => {
attrs.url = urlService.getUrlByResourceId(id, {absolute: true});
if (attrs.profile_image) {
attrs.profile_image = urlFor('image', {image: attrs.profile_image}, true);
}
if (attrs.cover_image) {
attrs.cover_image = urlFor('image', {image: attrs.cover_image}, true);
}
return attrs;
};
const forTag = (id, attrs) => {
attrs.url = urlService.getUrlByResourceId(id, {absolute: true});
if (attrs.feature_image) {
attrs.feature_image = urlFor('image', {image: attrs.feature_image}, true);
}
return attrs;
};
module.exports.forPost = forPost;
module.exports.forUser = forUser;
module.exports.forTag = forTag;

View File

@ -250,6 +250,47 @@ describe('Posts', function () {
});
});
it('browse posts: request only url fields', function (done) {
request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}&fields=url`))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
const jsonResponse = res.body;
should.exist(jsonResponse.posts);
testUtils.API.checkResponse(jsonResponse.posts[0], 'post', false, false, ['url']);
res.body.posts[0].url.should.eql('http://127.0.0.1:2369/welcome/');
done();
});
});
it('browse posts: request only url fields with include', function (done) {
request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}&fields=url&include=tags`))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
const jsonResponse = res.body;
should.exist(jsonResponse.posts);
testUtils.API.checkResponse(jsonResponse.posts[0], 'post', false, false, ['url','tags']);
jsonResponse.posts[0].url.should.eql('http://127.0.0.1:2369/welcome/');
jsonResponse.posts[0].tags[0].url.should.eql('http://127.0.0.1:2369/tag/getting-started/');
done();
});
});
it('browse posts: request to include tags and authors should always contain absolute urls', function (done) {
request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}&include=tags,authors`))
.set('Origin', testUtils.API.getURL())

View File

@ -10,9 +10,7 @@ describe('Unit: v2/utils/serializers/output/posts', function () {
beforeEach(function () {
postModel = (data) => {
return {
toJSON: sandbox.stub().returns(data)
};
return Object.assign(data, {toJSON: sandbox.stub().returns(data)});
};
sandbox.stub(urlService, 'getUrlByResourceId').returns('getUrlByResourceId');