Skip to content

Commit

Permalink
Content API v2 date formatting (#10095)
Browse files Browse the repository at this point in the history
closes #10065 

- Added UTC offset to dates returned by Content API
- Added test checking new format is compatible with Admin API
- Refactored output serializer mapping logic
  • Loading branch information
naz committed Nov 6, 2018
1 parent 7b38986 commit ec03b3c
Show file tree
Hide file tree
Showing 14 changed files with 488 additions and 132 deletions.
6 changes: 3 additions & 3 deletions core/server/api/v2/utils/serializers/output/pages.js
@@ -1,21 +1,21 @@
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:pages');
const url = require('./utils/url');
const mapper = require('./utils/mapper');

module.exports = {
all(models, apiConfig, frame) {
debug('all');

if (models.meta) {
frame.response = {
pages: models.data.map(model => url.forPost(model.id, model.toJSON(frame.options), frame.options)),
pages: models.data.map(model => mapper.mapPost(model, frame)),
meta: models.meta
};

return;
}

frame.response = {
pages: [url.forPost(models.id, models.toJSON(frame.options), frame.options)]
pages: [mapper.mapPost(models, frame)]
};

debug(frame.response);
Expand Down
6 changes: 3 additions & 3 deletions core/server/api/v2/utils/serializers/output/posts.js
@@ -1,5 +1,5 @@
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:posts');
const url = require('./utils/url');
const mapper = require('./utils/mapper');

module.exports = {
all(models, apiConfig, frame) {
Expand All @@ -12,7 +12,7 @@ module.exports = {

if (models.meta) {
frame.response = {
posts: models.data.map(model => url.forPost(model.id, model.toJSON(frame.options), frame.options)),
posts: models.data.map(model => mapper.mapPost(model, frame)),
meta: models.meta
};

Expand All @@ -21,7 +21,7 @@ module.exports = {
}

frame.response = {
posts: [url.forPost(models.id, models.toJSON(frame.options), frame.options)]
posts: [mapper.mapPost(models, frame)]
};

debug(frame.response);
Expand Down
6 changes: 3 additions & 3 deletions core/server/api/v2/utils/serializers/output/tags.js
@@ -1,5 +1,5 @@
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:tags');
const url = require('./utils/url');
const mapper = require('./utils/mapper');

module.exports = {
all(models, apiConfig, frame) {
Expand All @@ -11,15 +11,15 @@ module.exports = {

if (models.meta) {
frame.response = {
tags: models.data.map(model => url.forTag(model.id, model.toJSON(frame.options), frame.options)),
tags: models.data.map(model => mapper.mapTag(model, frame)),
meta: models.meta
};

return;
}

frame.response = {
tags: [url.forTag(models.id, models.toJSON(frame.options), frame.options)]
tags: [mapper.mapTag(models, frame)]
};

debug(frame.response);
Expand Down
6 changes: 3 additions & 3 deletions core/server/api/v2/utils/serializers/output/users.js
@@ -1,13 +1,13 @@
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:users');
const common = require('../../../../../lib/common');
const url = require('./utils/url');
const mapper = require('./utils/mapper');

module.exports = {
browse(models, apiConfig, frame) {
debug('browse');

frame.response = {
users: models.data.map(model => url.forUser(model.id, model.toJSON(frame.options), frame.options)),
users: models.data.map(model => mapper.mapUser(model, frame)),
meta: models.meta
};

Expand All @@ -18,7 +18,7 @@ module.exports = {
debug('read');

frame.response = {
users: [url.forUser(model.id, model.toJSON(frame.options), frame.options)]
users: [mapper.mapUser(model, frame)]
};

debug(frame.response);
Expand Down
32 changes: 32 additions & 0 deletions core/server/api/v2/utils/serializers/output/utils/date.js
@@ -0,0 +1,32 @@
const moment = require('moment-timezone');
const settingsCache = require('../../../../../../services/settings/cache');

const format = (date) => {
return moment(date)
.tz(settingsCache.get('active_timezone'))
.toISOString(true);
};

const forPost = (attrs) => {
['created_at', 'updated_at', 'published_at'].forEach((field) => {
if (attrs[field]) {
attrs[field] = format(attrs[field]);
}
});

return attrs;
};

const forTag = (attrs) => {
['created_at', 'updated_at'].forEach((field) => {
if (attrs[field]) {
attrs[field] = format(attrs[field]);
}
});

return attrs;
};

module.exports.format = format;
module.exports.forPost = forPost;
module.exports.forTag = forTag;
61 changes: 61 additions & 0 deletions core/server/api/v2/utils/serializers/output/utils/mapper.js
@@ -0,0 +1,61 @@
const utils = require('../../../index');
const url = require('./url');
const date = require('./date');

const mapPost = (model, frame) => {
const jsonModel = model.toJSON(frame.options);

url.forPost(model.id, jsonModel, frame.options);

if (utils.isContentAPI(frame)) {
date.forPost(jsonModel);
}

if (frame.options && frame.options.withRelated) {
frame.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' && jsonModel.tags) {
jsonModel.tags = jsonModel.tags.map(tag => url.forTag(tag.id, tag));

if (utils.isContentAPI(frame)) {
jsonModel.tags = jsonModel.tags.map(tag => date.forTag(tag));
}
}

if (relation === 'author' && jsonModel.author) {
jsonModel.author = url.forUser(jsonModel.author.id, jsonModel.author);
}

if (relation === 'authors' && jsonModel.authors) {
jsonModel.authors = jsonModel.authors.map(author => url.forUser(author.id, author));
}
});
}

return jsonModel;
};

const mapUser = (model, frame) => {
const jsonModel = model.toJSON(frame.options);

url.forUser(model.id, jsonModel);

return jsonModel;
};

const mapTag = (model, frame) => {
const jsonModel = model.toJSON(frame.options);
url.forTag(model.id, jsonModel);

if (utils.isContentAPI(frame)) {
date.forTag(jsonModel);
}

return jsonModel;
};

module.exports.mapPost = mapPost;
module.exports.mapUser = mapUser;
module.exports.mapTag = mapTag;
19 changes: 0 additions & 19 deletions core/server/api/v2/utils/serializers/output/utils/url.js
Expand Up @@ -36,25 +36,6 @@ const forPost = (id, attrs, options) => {
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;
};

Expand Down
12 changes: 9 additions & 3 deletions core/test/functional/api/v2/admin/posts_spec.js
Expand Up @@ -387,18 +387,20 @@ describe('Posts API V2', function () {
res.body.posts[0].title.should.eql(post.title);
res.body.posts[0].status.should.eql(post.status);
res.body.posts[0].published_at.should.eql('2016-05-30T07:00:00.000Z');
res.body.posts[0].published_at = '2016-05-30T09:00:00.000Z';
res.body.posts[0].created_at.should.not.eql(post.created_at.toISOString());
res.body.posts[0].updated_at.should.not.eql(post.updated_at.toISOString());
res.body.posts[0].updated_by.should.not.eql(post.updated_by);
res.body.posts[0].created_by.should.not.eql(post.created_by);
});
});

it('published post', function () {
it('published post with response timestamps in UTC format respecting original UTC offset', function () {
const post = {
posts: [{
status: 'published'
status: 'published',
published_at: '2016-05-31T07:00:00.000+06:00',
created_at: '2016-05-30T03:00:00.000Z',
updated_at: '2016-05-30T07:00:00.000'
}]
};

Expand All @@ -413,6 +415,10 @@ describe('Posts API V2', function () {
testUtils.API.checkResponse(res.body.posts[0], 'post');
res.body.posts[0].status.should.eql('published');
res.headers['x-cache-invalidate'].should.eql('/*');

res.body.posts[0].published_at.should.eql('2016-05-31T01:00:00.000Z');
res.body.posts[0].created_at.should.eql('2016-05-30T03:00:00.000Z');
res.body.posts[0].updated_at.should.eql('2016-05-30T07:00:00.000Z');
});
});
});
Expand Down
54 changes: 54 additions & 0 deletions core/test/unit/api/v2/utils/serializers/output/pages_spec.js
@@ -0,0 +1,54 @@
const should = require('should');
const sinon = require('sinon');
const testUtils = require('../../../../../../utils');
const mapper = require('../../../../../../../server/api/v2/utils/serializers/output/utils/mapper');
const serializers = require('../../../../../../../server/api/v2/utils/serializers');

const sandbox = sinon.sandbox.create();

describe('Unit: v2/utils/serializers/output/pages', () => {
let pageModel;

beforeEach(() => {
pageModel = (data) => {
return Object.assign(data, {toJSON: sandbox.stub().returns(data)});
};

sandbox.stub(mapper, 'mapPost').returns({});
});

afterEach(() => {
sandbox.restore();
});

it('calls the mapper', () => {
const apiConfig = {};
const frame = {
options: {
withRelated: ['tags', 'authors'],
context: {
private: false
}
}
};

const ctrlResponse = {
data: [
pageModel(testUtils.DataGenerator.forKnex.createPost({
id: 'id1',
page: true
})),
pageModel(testUtils.DataGenerator.forKnex.createPost({
id: 'id2',
page: true
}))
],
meta: {}
};

serializers.output.pages.all(ctrlResponse, apiConfig, frame);

mapper.mapPost.callCount.should.equal(2);
mapper.mapPost.getCall(0).args.should.eql([ctrlResponse.data[0], frame]);
});
});

0 comments on commit ec03b3c

Please sign in to comment.