From 1e624004653d5e19b480f7f36dd9437e2b12679f Mon Sep 17 00:00:00 2001 From: Sebastian Gierlinger Date: Sun, 27 Apr 2014 18:58:34 +0200 Subject: [PATCH] Add include parameter for posts API closes #2609 - added include parameter to api.posts.* - changed toJSON to omit objects that are not included - added include parameter to admin - added include parameter to frontend.js - updated tests - removed duplicate code from posts model **Known Issue:** It is not possible to attach a tag using an ID. --- core/client/models/post.js | 1 + core/client/models/themes.js | 2 +- core/client/router.js | 4 +- core/server/api/posts.js | 59 +++++++++++++++--- core/server/controllers/frontend.js | 6 +- core/server/models/base.js | 57 ++++++++++++------ core/server/models/post.js | 60 +++++++++++-------- core/test/functional/admin/content_test.js | 8 +-- core/test/functional/admin/editor_test.js | 6 +- core/test/functional/admin/flow_test.js | 4 +- core/test/functional/routes/api/posts_test.js | 35 +++++++++-- core/test/integration/api/api_db_spec.js | 3 +- .../integration/model/model_posts_spec.js | 10 ++-- 13 files changed, 182 insertions(+), 73 deletions(-) diff --git a/core/client/models/post.js b/core/client/models/post.js index f39a1b3280..67082b7535 100644 --- a/core/client/models/post.js +++ b/core/client/models/post.js @@ -49,6 +49,7 @@ if (method === 'create' || method === 'update') { options.data = JSON.stringify({posts: [this.attributes]}); options.contentType = 'application/json'; + options.url = model.url() + '?include=tags'; } return Backbone.Model.prototype.sync.apply(this, arguments); diff --git a/core/client/models/themes.js b/core/client/models/themes.js index 63e5f57931..86f19c0c13 100644 --- a/core/client/models/themes.js +++ b/core/client/models/themes.js @@ -3,7 +3,7 @@ 'use strict'; Ghost.Models.Themes = Backbone.Model.extend({ - url: Ghost.paths.apiRoot + '/themes' + url: Ghost.paths.apiRoot + '/themes/' }); }()); diff --git a/core/client/router.js b/core/client/router.js index 460f129c7a..9b19477337 100644 --- a/core/client/router.js +++ b/core/client/router.js @@ -36,7 +36,7 @@ blog: function () { var posts = new Ghost.Collections.Posts(); NProgress.start(); - posts.fetch({ data: { status: 'all', staticPages: 'all'} }).then(function () { + posts.fetch({ data: { status: 'all', staticPages: 'all', include: 'author'} }).then(function () { Ghost.currentView = new Ghost.Views.Blog({ el: '#main', collection: posts }); NProgress.done(); }); @@ -63,7 +63,7 @@ post.urlRoot = Ghost.paths.apiRoot + '/posts'; if (id) { post.id = id; - post.fetch({ data: {status: 'all'}}).then(function () { + post.fetch({ data: {status: 'all', include: 'tags'}}).then(function () { Ghost.currentView = new Ghost.Views.Editor({ el: '#main', model: post }); }); } else { diff --git a/core/server/api/posts.js b/core/server/api/posts.js index da53b5ad53..faf4ef5366 100644 --- a/core/server/api/posts.js +++ b/core/server/api/posts.js @@ -3,7 +3,9 @@ var when = require('when'), dataProvider = require('../models'), canThis = require('../permissions').canThis, filteredUserAttributes = require('./users').filteredAttributes, - posts; + + posts, + allowedIncludes = ['created_by', 'updated_by', 'published_by', 'author', 'tags', 'fields']; function checkPostData(postData) { if (_.isEmpty(postData) || _.isEmpty(postData.posts) || _.isEmpty(postData.posts[0])) { @@ -12,6 +14,19 @@ function checkPostData(postData) { return when.resolve(postData); } +function prepareInclude(include) { + var index; + + include = _.intersection(include.split(","), allowedIncludes); + index = include.indexOf('author'); + + if (index !== -1) { + include[index] = 'author_id'; + } + + return include; +} + // ## Posts posts = { @@ -24,13 +39,20 @@ posts = { if (!this.user) { options.status = 'published'; } + + if (options.include) { + options.include = prepareInclude(options.include); + } + // **returns:** a promise for a page of posts in a json object return dataProvider.Post.findPage(options).then(function (result) { var i = 0, omitted = result; for (i = 0; i < omitted.posts.length; i = i + 1) { - omitted.posts[i].author = _.omit(omitted.posts[i].author, filteredUserAttributes); + if (!_.isNumber(omitted.posts[i].author)) { + omitted.posts[i].author = _.omit(omitted.posts[i].author, filteredUserAttributes); + } } return omitted; }); @@ -39,6 +61,7 @@ posts = { // #### Read // **takes:** an identifier (id or slug?) read: function read(options) { + var include; options = options || {}; // only published posts if no user is present @@ -46,15 +69,23 @@ posts = { options.status = 'published'; } + if (options.include) { + include = prepareInclude(options.include); + delete options.include; + } + // **returns:** a promise for a single post in a json object - return dataProvider.Post.findOne(options).then(function (result) { + return dataProvider.Post.findOne(options, {include: include}).then(function (result) { var omitted; if (result) { omitted = result.toJSON(); - omitted.author = _.omit(omitted.author, filteredUserAttributes); + if (!_.isNumber(omitted.author)) { + omitted.author = _.omit(omitted.author, filteredUserAttributes); + } return { posts: [ omitted ]}; } + return when.reject({code: 404, message: 'Post not found'}); }); @@ -64,21 +95,30 @@ posts = { // **takes:** a json object with all the properties which should be updated edit: function edit(postData) { // **returns:** a promise for the resulting post in a json object - var self = this; + var self = this, + include; return canThis(self.user).edit.post(postData.id).then(function () { return checkPostData(postData).then(function (checkedPostData) { - return dataProvider.Post.edit(checkedPostData.posts[0], {user: self.user}); + + if (postData.include) { + include = prepareInclude(postData.include); + } + + return dataProvider.Post.edit(checkedPostData.posts[0], {user: self.user, include: include}); }).then(function (result) { if (result) { var omitted = result.toJSON(); - omitted.author = _.omit(omitted.author, filteredUserAttributes); + if (!_.isNumber(omitted.author)) { + omitted.author = _.omit(omitted.author, filteredUserAttributes); + } // If previously was not published and now is, signal the change if (result.updated('status') !== result.get('status')) { omitted.statusChanged = true; } return { posts: [ omitted ]}; } + return when.reject({code: 404, message: 'Post not found'}); }); }, function () { @@ -96,7 +136,9 @@ posts = { return dataProvider.Post.add(checkedPostData.posts[0], {user: self.user}); }).then(function (result) { var omitted = result.toJSON(); - omitted.author = _.omit(omitted.author, filteredUserAttributes); + if (!_.isNumber(omitted.author)) { + omitted.author = _.omit(omitted.author, filteredUserAttributes); + } if (omitted.status === 'published') { // When creating a new post that is published right now, signal the change omitted.statusChanged = true; @@ -134,7 +176,6 @@ posts = { }, // #### Generate slug - // **takes:** a string to generate the slug from generateSlug: function generateSlug(args) { diff --git a/core/server/controllers/frontend.js b/core/server/controllers/frontend.js index b806069cfd..1cb13ef92c 100644 --- a/core/server/controllers/frontend.js +++ b/core/server/controllers/frontend.js @@ -28,7 +28,7 @@ function getPostPage(options) { if (!isNaN(postsPerPage) && postsPerPage > 0) { options.limit = postsPerPage; } - + options.include = 'author,tags,fields'; return api.posts.browse(options); }).then(function (page) { @@ -162,6 +162,8 @@ frontendControllers = { // Sanitize params we're going to use to lookup the post. var postLookup = _.pick(permalink.params, 'slug', 'id'); + // Add author, tag and fields + postLookup.include = 'author,tags,fields'; // Query database to find post return api.posts.read(postLookup); @@ -270,6 +272,8 @@ frontendControllers = { if (pageParam) { options.page = pageParam; } if (tagParam) { options.tag = tagParam; } + options.include = 'author,tags,fields'; + return api.posts.browse(options).then(function (page) { var title = result[0].value.value, diff --git a/core/server/models/base.js b/core/server/models/base.js index 71f1325995..9766ba4ef3 100644 --- a/core/server/models/base.js +++ b/core/server/models/base.js @@ -1,13 +1,13 @@ -var Bookshelf = require('bookshelf'), - when = require('when'), - moment = require('moment'), - _ = require('lodash'), - uuid = require('node-uuid'), - config = require('../config'), - unidecode = require('unidecode'), - sanitize = require('validator').sanitize, - schema = require('../data/schema'), - validation = require('../data/validation'), +var Bookshelf = require('bookshelf'), + when = require('when'), + moment = require('moment'), + _ = require('lodash'), + uuid = require('node-uuid'), + config = require('../config'), + unidecode = require('unidecode'), + sanitize = require('validator').sanitize, + schema = require('../data/schema'), + validation = require('../data/validation'), ghostBookshelf; @@ -15,7 +15,6 @@ var Bookshelf = require('bookshelf'), ghostBookshelf = Bookshelf.ghost = Bookshelf.initialize(config().database); ghostBookshelf.client = config().database.client; - // The Base Model which other Ghost objects will inherit from, // including some convenience functions as static properties on the model. ghostBookshelf.Model = ghostBookshelf.Model.extend({ @@ -34,7 +33,14 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ }, initialize: function () { - var self = this; + var self = this, + options = arguments[1] || {}; + + // make options include available for toJSON() + if (options.include) { + this.include = _.clone(options.include); + } + this.on('creating', this.creating, this); this.on('saving', function (model, attributes, options) { return when(self.saving(model, attributes, options)).then(function () { @@ -98,15 +104,25 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ toJSON: function (options) { var attrs = _.extend({}, this.attributes), - relations = this.relations; + self = this; if (options && options.shallow) { return attrs; } - _.each(relations, function (relation, key) { + if (options && options.idOnly) { + return attrs.id; + } + + _.each(this.relations, function (relation, key) { if (key.substring(0, 7) !== '_pivot_') { - attrs[key] = relation.toJSON ? relation.toJSON() : relation; + // if include is set, expand to full object + // toMany relationships are included with ids if not expanded + if (_.contains(self.include, key)) { + attrs[key] = relation.toJSON(); + } else if (relation.hasOwnProperty('length')) { + attrs[key] = relation.toJSON({idOnly: true}); + } } }); @@ -135,7 +151,14 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ */ findAll: function (options) { options = options || {}; - return ghostBookshelf.Collection.forge([], {model: this}).fetch(options); + return ghostBookshelf.Collection.forge([], {model: this}).fetch(options).then(function (result) { + if (options.include) { + _.each(result.models, function (item) { + item.include = options.include; + }); + } + return result; + }); }, browse: function () { @@ -149,7 +172,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ */ findOne: function (args, options) { options = options || {}; - return this.forge(args).fetch(options); + return this.forge(args, {include: options.include}).fetch(options); }, read: function () { diff --git a/core/server/models/post.js b/core/server/models/post.js index 0fa82e4ca5..b647afaa5c 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -10,7 +10,6 @@ var _ = require('lodash'), Tag = require('./tag').Tag, Tags = require('./tag').Tags, ghostBookshelf = require('./base'), - validation = require('../data/validation'), xmlrpc = require('../xmlrpc'), Post, @@ -29,22 +28,15 @@ Post = ghostBookshelf.Model.extend({ initialize: function () { var self = this; - this.on('creating', this.creating, this); + + ghostBookshelf.Model.prototype.initialize.apply(this, arguments); + this.on('saved', function (model, attributes, options) { if (model.get('status') === 'published') { xmlrpc.ping(model.attributes); } return self.updateTags(model, attributes, options); }); - this.on('saving', function (model, attributes, options) { - return when(self.saving(model, attributes, options)).then(function () { - return self.validate(model, attributes, options); - }); - }); - }, - - validate: function () { - validation.validateSchema(this.tableName, this.toJSON()); }, saving: function (newPage, attr, options) { @@ -57,13 +49,16 @@ Post = ghostBookshelf.Model.extend({ // keep tags for 'saved' event and deduplicate upper/lowercase tags tagsToCheck = this.get('tags'); this.myTags = []; + _.each(tagsToCheck, function (item) { - for (i = 0; i < self.myTags.length; i = i + 1) { - if (self.myTags[i].name.toLocaleLowerCase() === item.name.toLocaleLowerCase()) { - return; + if (_.isObject(self.myTags)) { + for (i = 0; i < self.myTags.length; i = i + 1) { + if (self.myTags[i].name.toLocaleLowerCase() === item.name.toLocaleLowerCase()) { + return; + } } + self.myTags.push(item); } - self.myTags.push(item); }); ghostBookshelf.Model.prototype.saving.call(this, newPage, attr, options); @@ -201,10 +196,22 @@ Post = ghostBookshelf.Model.extend({ }, // Relations - author: function () { + author_id: function () { return this.belongsTo(User, 'author_id'); }, + created_by: function () { + return this.belongsTo(User, 'created_by'); + }, + + updated_by: function () { + return this.belongsTo(User, 'updated_by'); + }, + + published_by: function () { + return this.belongsTo(User, 'published_by'); + }, + tags: function () { return this.belongsToMany(Tag); }, @@ -228,8 +235,7 @@ Post = ghostBookshelf.Model.extend({ // Extends base model findAll to eager-fetch author and user relationships. findAll: function (options) { options = options || {}; - - options.withRelated = [ 'author', 'tags', 'fields' ]; + options.withRelated = _.union([ 'tags', 'fields' ], options.include); return ghostBookshelf.Model.findAll.call(this, options); }, @@ -246,7 +252,9 @@ Post = ghostBookshelf.Model.extend({ delete args.status; } - options.withRelated = [ 'author', 'tags', 'fields' ]; + // Add related objects + options.withRelated = _.union([ 'tags', 'fields' ], options.include); + return ghostBookshelf.Model.findOne.call(this, args, options); }, @@ -273,7 +281,7 @@ Post = ghostBookshelf.Model.extend({ findPage: function (opts) { var postCollection = Posts.forge(), tagInstance = opts.tag !== undefined ? Tag.forge({slug: opts.tag}) : false, - permittedOptions = ['page', 'limit', 'status', 'staticPages']; + permittedOptions = ['page', 'limit', 'status', 'staticPages', 'include']; // sanitize opts so we are not automatically passing through any and all // query strings to Bookshelf / Knex. Although the API requires auth, we @@ -311,8 +319,8 @@ Post = ghostBookshelf.Model.extend({ postCollection.query('where', opts.where); } - // Fetch related models - opts.withRelated = [ 'author', 'tags', 'fields' ]; + // Add related objects + opts.withRelated = _.union([ 'tags', 'fields' ], opts.include); // If a query param for a tag is attached // we need to fetch the tag model to find its id @@ -338,7 +346,6 @@ Post = ghostBookshelf.Model.extend({ .query('join', 'posts_tags', 'posts_tags.post_id', '=', 'posts.id') .query('where', 'posts_tags.tag_id', '=', tagInstance.id); } - return postCollection .query('limit', opts.limit) .query('offset', opts.limit * (opts.page - 1)) @@ -384,6 +391,12 @@ Post = ghostBookshelf.Model.extend({ pagination['next'] = null; pagination['prev'] = null; + if (opts.include) { + _.each(postCollection.models, function (item) { + item.include = opts.include; + }); + } + data['posts'] = postCollection.toJSON(); data['meta'] = meta; meta['pagination'] = pagination; @@ -441,7 +454,6 @@ Post = ghostBookshelf.Model.extend({ edit: function (editedPost, options) { var self = this; options = options || {}; - return ghostBookshelf.Model.edit.call(this, editedPost, options).then(function (post) { if (post) { return self.findOne({status: 'all', id: post.id}, options) diff --git a/core/test/functional/admin/content_test.js b/core/test/functional/admin/content_test.js index c378011d45..f013f07fe1 100644 --- a/core/test/functional/admin/content_test.js +++ b/core/test/functional/admin/content_test.js @@ -17,7 +17,7 @@ CasperTest.begin("Content screen is correct", 22, function suite(test) { casper.thenClick('.js-publish-button'); - casper.waitForResource(/posts\/$/, function checkPostWasCreated() { + casper.waitForResource(/posts\/\?include=tags$/, function checkPostWasCreated() { test.assertExists('.notification-success', 'got success notification'); }); @@ -97,7 +97,7 @@ CasperTest.begin('Content list shows correct post status', 8, function testStati test.assert(false, 'publish button did not change to published'); }); - casper.waitForResource(/posts\/$/, function checkPostWasCreated() { + casper.waitForResource(/posts\/\?include=tags$/, function checkPostWasCreated() { test.assertExists('.notification-success', 'got success notification'); }); @@ -159,7 +159,7 @@ CasperTest.begin('Preview shows correct header for published post', 7, function test.assert(false, 'publish button did not change to published'); }); - casper.waitForResource(/posts\/$/, function checkPostWasCreated() { + casper.waitForResource(/posts\/\?include=tags$/, function checkPostWasCreated() { test.assertExists('.notification-success', 'got success notification'); }); @@ -196,7 +196,7 @@ CasperTest.begin('Delete post modal', 9, function testDeleteModal(test) { casper.thenClick('.js-publish-button'); - casper.waitForResource(/posts\/$/, function checkPostWasCreated() { + casper.waitForResource(/posts\/\?include=tags$/, function checkPostWasCreated() { test.assertExists('.notification-success', 'got success notification'); }); diff --git a/core/test/functional/admin/editor_test.js b/core/test/functional/admin/editor_test.js index 3d99e833d5..53b51ab92a 100644 --- a/core/test/functional/admin/editor_test.js +++ b/core/test/functional/admin/editor_test.js @@ -33,7 +33,7 @@ CasperTest.begin("Ghost editor is correct", 10, function suite(test) { casper.thenClick('.js-publish-button'); - casper.waitForResource(/\/posts\/$/, function checkPostWasCreated() { + casper.waitForResource(/posts\/\?include=tags$/, function checkPostWasCreated() { var urlRegExp = new RegExp("^" + escapedUrl + "ghost\/editor\/[0-9]*"); test.assertUrlMatch(urlRegExp, 'got an id on our URL'); test.assertExists('.notification-success', 'got success notification'); @@ -376,7 +376,7 @@ CasperTest.begin('Publish menu - existing post', 22, function suite(test) { // Create a post in draft status casper.thenClick('.js-publish-button'); - casper.waitForResource(/posts\/$/, function checkPostWasCreated() { + casper.waitForResource(/posts\/\?include=tags$/, function checkPostWasCreated() { var urlRegExp = new RegExp("^" + escapedUrl + "ghost\/editor\/[0-9]*"); test.assertUrlMatch(urlRegExp, 'got an id on our URL'); }); @@ -413,7 +413,7 @@ CasperTest.begin('Publish menu - existing post', 22, function suite(test) { // Publish the post casper.thenClick('.js-publish-button'); - casper.waitForResource(/posts\/$/, function checkPostWasCreated() { + casper.waitForResource(/posts\/\?include=tags$/, function checkPostWasCreated() { var urlRegExp = new RegExp("^" + escapedUrl + "ghost\/editor\/[0-9]*"); test.assertUrlMatch(urlRegExp, 'got an id on our URL'); }); diff --git a/core/test/functional/admin/flow_test.js b/core/test/functional/admin/flow_test.js index cb0b9ccd61..2617a9b624 100644 --- a/core/test/functional/admin/flow_test.js +++ b/core/test/functional/admin/flow_test.js @@ -18,7 +18,7 @@ CasperTest.begin("Ghost edit draft flow works correctly", 8, function suite(test }); casper.thenClick('.js-publish-button'); - casper.waitForResource(/posts\/$/); + casper.waitForResource(/posts\/\?include=tags$/); casper.waitForSelector('.notification-success', function onSuccess() { test.assert(true, 'Got success notification'); @@ -43,7 +43,7 @@ CasperTest.begin("Ghost edit draft flow works correctly", 8, function suite(test }); casper.thenClick('.js-publish-button'); - casper.waitForResource(/posts\/[0-9]+\/$/); + casper.waitForResource(/posts\/[0-9]+\/\?include=tags$/); casper.waitForSelector('.notification-success', function onSuccess() { test.assert(true, 'Got success notification'); diff --git a/core/test/functional/routes/api/posts_test.js b/core/test/functional/routes/api/posts_test.js index d0dce26867..f1434fd420 100644 --- a/core/test/functional/routes/api/posts_test.js +++ b/core/test/functional/routes/api/posts_test.js @@ -210,6 +210,33 @@ describe('Post API', function () { jsonResponse.posts[0].page.should.eql(0); _.isBoolean(jsonResponse.posts[0].featured).should.eql(true); _.isBoolean(jsonResponse.posts[0].page).should.eql(true); + jsonResponse.posts[0].author.should.be.a.Number; + jsonResponse.posts[0].created_by.should.be.a.Number; + jsonResponse.posts[0].tags[0].should.be.a.Number; + done(); + }); + }); + + it('can retrieve a post with author, created_by, and tags', function (done) { + request.get(testUtils.API.getApiQuery('posts/1/?include=author,tags,created_by')) + .end(function (err, res) { + if (err) { + return done(err); + } + + res.should.have.status(200); + should.not.exist(res.headers['x-cache-invalidate']); + res.should.be.json; + var jsonResponse = res.body; + jsonResponse.should.exist; + jsonResponse.posts.should.exist; + testUtils.API.checkResponse(jsonResponse.posts[0], 'post'); + jsonResponse.posts[0].page.should.eql(0); + + jsonResponse.posts[0].author.should.be.an.Object; + testUtils.API.checkResponse(jsonResponse.posts[0].author, 'user'); + jsonResponse.posts[0].tags[0].should.be.an.Object; + testUtils.API.checkResponse(jsonResponse.posts[0].tags[0], 'tag'); done(); }); }); @@ -358,7 +385,7 @@ describe('Post API', function () { // ## edit describe('Edit', function () { it('can edit a post', function (done) { - request.get(testUtils.API.getApiQuery('posts/1/')) + request.get(testUtils.API.getApiQuery('posts/1/?include=tags')) .end(function (err, res) { if (err) { return done(err); @@ -391,7 +418,7 @@ describe('Post API', function () { }); it('can change a post to a static page', function (done) { - request.get(testUtils.API.getApiQuery('posts/1/')) + request.get(testUtils.API.getApiQuery('posts/1/?include=tags')) .end(function (err, res) { if (err) { return done(err); @@ -482,7 +509,7 @@ describe('Post API', function () { }); it('published_at = null', function (done) { - request.get(testUtils.API.getApiQuery('posts/1/')) + request.get(testUtils.API.getApiQuery('posts/1/?include=tags')) .end(function (err, res) { if (err) { return done(err); @@ -703,7 +730,7 @@ describe('Post API', function () { }); it('Can edit a post', function (done) { - request.get(testUtils.API.getApiQuery('posts/2/')) + request.get(testUtils.API.getApiQuery('posts/2/?include=tags')) .end(function (err, res) { if (err) { return done(err); diff --git a/core/test/integration/api/api_db_spec.js b/core/test/integration/api/api_db_spec.js index 5f11154976..69b86c7afb 100644 --- a/core/test/integration/api/api_db_spec.js +++ b/core/test/integration/api/api_db_spec.js @@ -44,7 +44,8 @@ describe('DB API', function () { }).then(function () { TagsAPI.browse().then(function (results) { should.exist(results); - results.length.should.equal(0); + should.exist(results.tags); + results.tags.length.should.equal(0); }); }).then(function () { PostAPI.browse().then(function (results) { diff --git a/core/test/integration/model/model_posts_spec.js b/core/test/integration/model/model_posts_spec.js index 4ca6327b8b..7ed2c24d80 100644 --- a/core/test/integration/model/model_posts_spec.js +++ b/core/test/integration/model/model_posts_spec.js @@ -65,10 +65,10 @@ describe('Post Model', function () { }).then(null, done); }); - it('can findAll, returning author, user and field data', function (done) { + it('can findAll, returning author and field data', function (done) { var firstPost; - PostModel.findAll({}).then(function (results) { + PostModel.findAll({include: ['author_id', 'fields']}).then(function (results) { should.exist(results); results.length.should.be.above(0); firstPost = results.models[0].toJSON(); @@ -83,10 +83,10 @@ describe('Post Model', function () { }, done); }); - it('can findOne, returning author, user and field data', function (done) { + it('can findOne, returning author and field data', function (done) { var firstPost; - - PostModel.findOne({}).then(function (result) { + // TODO: should take author :-/ + PostModel.findOne({}, {include: ['author_id', 'fields']}).then(function (result) { should.exist(result); firstPost = result.toJSON();