diff --git a/core/server/models/base/index.js b/core/server/models/base/index.js index 8f53ca1c68..6350044af0 100644 --- a/core/server/models/base/index.js +++ b/core/server/models/base/index.js @@ -136,6 +136,8 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ * @returns {*} */ onValidate: function onValidate(model, columns, options) { + this.setEmptyValuesToNull(); + return validation.validateSchema(this.tableName, this, options); }, @@ -288,6 +290,24 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ return attrs; }, + // Sets given values to `null` + setEmptyValuesToNull: function setEmptyValuesToNull() { + var self = this, + attr; + + if (!this.emptyStringProperties) { + return; + } + + attr = this.emptyStringProperties(); + + _.each(attr, function (value) { + if (self.get(value) === '') { + self.set(value, null); + } + }); + }, + // Get the user from the options object contextUser: function contextUser(options) { options = options || {}; diff --git a/core/server/models/post.js b/core/server/models/post.js index cf3d08cc8e..8d1a39cd73 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -302,6 +302,13 @@ Post = ghostBookshelf.Model.extend({ return sequence(ops); }, + emptyStringProperties: function emptyStringProperties() { + // CASE: the client might send empty image properties with "" instead of setting them to null. + // This can cause GQL to fail. We therefore enforce 'null' for empty image properties. + // See https://github.com/TryGhost/GQL/issues/24 + return ['feature_image', 'og_image', 'twitter_image']; + }, + onCreating: function onCreating(model, attr, options) { options = options || {}; diff --git a/core/server/models/tag.js b/core/server/models/tag.js index e9d667541c..cc8d316584 100644 --- a/core/server/models/tag.js +++ b/core/server/models/tag.js @@ -49,6 +49,13 @@ Tag = ghostBookshelf.Model.extend({ } }, + emptyStringProperties: function emptyStringProperties() { + // CASE: the client might send empty image properties with "" instead of setting them to null. + // This can cause GQL to fail. We therefore enforce 'null' for empty image properties. + // See https://github.com/TryGhost/GQL/issues/24 + return ['feature_image']; + }, + posts: function posts() { return this.belongsToMany('Post'); }, diff --git a/core/server/models/user.js b/core/server/models/user.js index 1cd58b2e7b..14eecd5cb9 100644 --- a/core/server/models/user.js +++ b/core/server/models/user.js @@ -211,6 +211,13 @@ User = ghostBookshelf.Model.extend({ return attrs; }, + emptyStringProperties: function emptyStringProperties() { + // CASE: the client might send empty image properties with "" instead of setting them to null. + // This can cause GQL to fail. We therefore enforce 'null' for empty image properties. + // See https://github.com/TryGhost/GQL/issues/24 + return ['profile_image', 'cover_image']; + }, + format: function format(options) { if (!_.isEmpty(options.website) && !validator.isURL(options.website, { diff --git a/core/test/unit/models/base/index_spec.js b/core/test/unit/models/base/index_spec.js new file mode 100644 index 0000000000..a9c43b3d8b --- /dev/null +++ b/core/test/unit/models/base/index_spec.js @@ -0,0 +1,42 @@ +'use strict'; + +var should = require('should'), + sinon = require('sinon'), + _ = require('lodash'), + models = require('../../../../server/models'), + ghostBookshelf, + testUtils = require('../../../utils'), + + sandbox = sinon.sandbox.create(); + +describe('Models: base', function () { + before(function () { + models.init(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('setEmptyValuesToNull', function () { + it('resets given empty value to null', function () { + const base = models.Base.Model.forge({a: '', b: ''}); + + base.emptyStringProperties = sandbox.stub(); + base.emptyStringProperties.returns(['a']); + + base.get('a').should.eql(''); + base.get('b').should.eql(''); + base.setEmptyValuesToNull(); + should.not.exist(base.get('a')); + base.get('b').should.eql(''); + }); + + it('does not reset to null if model does\'t provide properties', function () { + const base = models.Base.Model.forge({a: ''}); + base.get('a').should.eql(''); + base.setEmptyValuesToNull(); + base.get('a').should.eql(''); + }); + }); +}); diff --git a/core/test/unit/models/post_spec.js b/core/test/unit/models/post_spec.js index eb827828dd..4a8a7a7626 100644 --- a/core/test/unit/models/post_spec.js +++ b/core/test/unit/models/post_spec.js @@ -1,16 +1,49 @@ +'use strict'; + var should = require('should'), // jshint ignore:line sinon = require('sinon'), models = require('../../../server/models'), common = require('../../../server/lib/common'), - utils = require('../../utils'), - + testUtils = require('../../utils'), sandbox = sinon.sandbox.create(); -describe('Models: Post', function () { +describe('Unit: models/post', function () { before(function () { models.init(); }); + after(function () { + sandbox.restore(); + }); + + describe('Edit', function () { + let knexMock; + + before(function () { + knexMock = new testUtils.mocks.knex(); + knexMock.mock(); + }); + + after(function () { + knexMock.unmock(); + }); + + it('resets given empty value to null', function () { + return models.Post.findOne({slug: 'html-ipsum'}) + .then(function (post) { + post.get('slug').should.eql('html-ipsum'); + post.get('feature_image').should.eql('https://example.com/super_photo.jpg'); + post.set('feature_image', ''); + post.set('custom_excerpt', ''); + return post.save(); + }) + .then(function (post) { + should(post.get('feature_image')).be.null(); + post.get('custom_excerpt').should.eql(''); + }); + }); + }); + describe('Permissible', function () { describe('As Contributor', function () { describe('Editing', function () { @@ -29,7 +62,7 @@ describe('Models: Post', function () { 'edit', context, unsafeAttrs, - utils.permissions.contributor, + testUtils.permissions.contributor, false, false ).then(() => { @@ -56,7 +89,7 @@ describe('Models: Post', function () { 'edit', context, unsafeAttrs, - utils.permissions.contributor, + testUtils.permissions.contributor, false, true ).then(() => { @@ -83,7 +116,7 @@ describe('Models: Post', function () { 'edit', context, unsafeAttrs, - utils.permissions.contributor, + testUtils.permissions.contributor, false, true ).then(() => { @@ -110,7 +143,7 @@ describe('Models: Post', function () { 'edit', context, unsafeAttrs, - utils.permissions.contributor, + testUtils.permissions.contributor, false, true ).then(() => { @@ -137,7 +170,7 @@ describe('Models: Post', function () { 'edit', context, unsafeAttrs, - utils.permissions.contributor, + testUtils.permissions.contributor, false, true ).then((result) => { @@ -161,7 +194,7 @@ describe('Models: Post', function () { 'add', context, unsafeAttrs, - utils.permissions.contributor, + testUtils.permissions.contributor, false, true ).then(() => { @@ -185,7 +218,7 @@ describe('Models: Post', function () { 'add', context, unsafeAttrs, - utils.permissions.contributor, + testUtils.permissions.contributor, false, true ).then(() => { @@ -209,7 +242,7 @@ describe('Models: Post', function () { 'add', context, unsafeAttrs, - utils.permissions.contributor, + testUtils.permissions.contributor, false, true ).then((result) => { @@ -234,7 +267,7 @@ describe('Models: Post', function () { 'destroy', context, {}, - utils.permissions.contributor, + testUtils.permissions.contributor, false, true ).then(() => { @@ -260,7 +293,7 @@ describe('Models: Post', function () { 'destroy', context, {}, - utils.permissions.contributor, + testUtils.permissions.contributor, false, true ).then(() => { @@ -286,7 +319,7 @@ describe('Models: Post', function () { 'destroy', context, {}, - utils.permissions.contributor, + testUtils.permissions.contributor, false, true ).then((result) => { @@ -314,7 +347,7 @@ describe('Models: Post', function () { 'edit', context, unsafeAttrs, - utils.permissions.author, + testUtils.permissions.author, false, true ).then(() => { @@ -340,7 +373,7 @@ describe('Models: Post', function () { 'edit', context, unsafeAttrs, - utils.permissions.author, + testUtils.permissions.author, false, true ).then(() => { @@ -366,7 +399,7 @@ describe('Models: Post', function () { 'edit', context, unsafeAttrs, - utils.permissions.author, + testUtils.permissions.author, false, true ).then(() => { @@ -388,7 +421,7 @@ describe('Models: Post', function () { 'add', context, unsafeAttrs, - utils.permissions.author, + testUtils.permissions.author, false, true ).then(() => { @@ -412,7 +445,7 @@ describe('Models: Post', function () { 'add', context, unsafeAttrs, - utils.permissions.author, + testUtils.permissions.author, false, true ).then(() => { @@ -437,7 +470,7 @@ describe('Models: Post', function () { 'edit', context, unsafeAttrs, - utils.permissions.editor, + testUtils.permissions.editor, false, true ).then(() => { @@ -463,7 +496,7 @@ describe('Models: Post', function () { 'edit', context, unsafeAttrs, - utils.permissions.editor, + testUtils.permissions.editor, true, true ).then(() => { diff --git a/core/test/unit/models/tag_spec.js b/core/test/unit/models/tag_spec.js new file mode 100644 index 0000000000..b5ccccddb6 --- /dev/null +++ b/core/test/unit/models/tag_spec.js @@ -0,0 +1,45 @@ +'use strict'; + +var should = require('should'), // jshint ignore:line + sinon = require('sinon'), + models = require('../../../server/models'), + testUtils = require('../../utils'), + sandbox = sinon.sandbox.create(); + +describe('Unit: models/tags', function () { + before(function () { + models.init(); + }); + + after(function () { + sandbox.restore(); + }); + + describe('Edit', function () { + let knexMock; + + before(function () { + knexMock = new testUtils.mocks.knex(); + knexMock.mock(); + }); + + after(function () { + knexMock.unmock(); + }); + + it('resets given empty value to null', function () { + return models.Tag.findOne({slug: 'kitchen-sink'}) + .then(function (tag) { + tag.get('slug').should.eql('kitchen-sink'); + tag.get('feature_image').should.eql('https://example.com/super_photo.jpg'); + tag.set('feature_image', ''); + tag.set('description', ''); + return tag.save(); + }) + .then(function (tag) { + should(tag.get('feature_image')).be.null(); + tag.get('description').should.eql(''); + }); + }); + }); +}); diff --git a/core/test/unit/models/user_spec.js b/core/test/unit/models/user_spec.js index 43f3997732..71f2ba23ee 100644 --- a/core/test/unit/models/user_spec.js +++ b/core/test/unit/models/user_spec.js @@ -308,4 +308,40 @@ describe('Unit: models/user', function () { }); }); }); + + describe('Edit', function () { + let knexMock; + + before(function () { + models.init(); + }); + + after(function () { + sandbox.restore(); + }); + + before(function () { + knexMock = new testUtils.mocks.knex(); + knexMock.mock(); + }); + + after(function () { + knexMock.unmock(); + }); + + it('resets given empty value to null', function () { + return models.User.findOne({slug: 'joe-bloggs'}) + .then(function (user) { + user.get('slug').should.eql('joe-bloggs'); + user.get('profile_image').should.eql('https://example.com/super_photo.jpg'); + user.set('profile_image', ''); + user.set('bio', ''); + return user.save(); + }) + .then(function (user) { + should(user.get('profile_image')).be.null(); + user.get('bio').should.eql(''); + }); + }); + }); }); diff --git a/core/test/utils/fixtures/data-generator.js b/core/test/utils/fixtures/data-generator.js index 653420557d..b98f5530b4 100644 --- a/core/test/utils/fixtures/data-generator.js +++ b/core/test/utils/fixtures/data-generator.js @@ -28,66 +28,67 @@ DataGenerator.Content = { posts: [ { id: ObjectId.generate(), - title: "HTML Ipsum", - slug: "html-ipsum", - mobiledoc: DataGenerator.markdownToMobiledoc("
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae
, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.
#header h1 a{display: block;width: 300px;height: 80px;}
"),
- published_at: new Date("2015-01-01"),
- custom_excerpt: 'This is my custom excerpt!'
+ title: 'HTML Ipsum',
+ slug: 'html-ipsum',
+ mobiledoc: DataGenerator.markdownToMobiledoc('Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae
, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.
#header h1 a{display: block;width: 300px;height: 80px;}
'),
+ published_at: new Date('2015-01-01'),
+ custom_excerpt: 'This is my custom excerpt!',
+ feature_image: 'https://example.com/super_photo.jpg'
},
{
id: ObjectId.generate(),
- title: "Ghostly Kitchen Sink",
- slug: "ghostly-kitchen-sink",
- mobiledoc: DataGenerator.markdownToMobiledoc("Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae
, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.
#header h1 a{display: block;width: 300px;height: 80px;}
"),
- published_at: new Date("2015-01-02")
+ title: 'Ghostly Kitchen Sink',
+ slug: 'ghostly-kitchen-sink',
+ mobiledoc: DataGenerator.markdownToMobiledoc('Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae
, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.
#header h1 a{display: block;width: 300px;height: 80px;}
'),
+ published_at: new Date('2015-01-02')
},
{
id: ObjectId.generate(),
- title: "Short and Sweet",
- slug: "short-and-sweet",
- mobiledoc: DataGenerator.markdownToMobiledoc("## testing\n\nmctesters\n\n- test\n- line\n- items"),
- html: "mctesters
\nmctesters
\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae
, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.
#header h1 a{display: block;width: 300px;height: 80px;}
"),
- status: "draft",
- uuid: "d52c42ae-2755-455c-80ec-70b2ec55c903",
+ title: 'Not finished yet',
+ slug: 'unfinished',
+ mobiledoc: DataGenerator.markdownToMobiledoc('Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae
, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.
#header h1 a{display: block;width: 300px;height: 80px;}
'),
+ status: 'draft',
+ uuid: 'd52c42ae-2755-455c-80ec-70b2ec55c903',
featured: false
},
{
id: ObjectId.generate(),
- title: "Not so short, bit complex",
- slug: "not-so-short-bit-complex",
- mobiledoc: DataGenerator.markdownToMobiledoc("Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.
1 | 2 | 3 | 4 |
---|---|---|---|
a | b | c | d |
e | f | g | h |
i | j | k | l |
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.
1 | 2 | 3 | 4 |
---|---|---|---|
a | b | c | d |
e | f | g | h |
i | j | k | l |
Hopefully you don't find it a bore.
"), + title: 'This is a static page', + slug: 'static-page-test', + mobiledoc: DataGenerator.markdownToMobiledoc('Hopefully you don\'t find it a bore.
'), page: 1 }, { id: ObjectId.generate(), - title: "This is a draft static page", - slug: "static-page-draft", - mobiledoc: DataGenerator.markdownToMobiledoc("Hopefully you don't find it a bore.
"), + title: 'This is a draft static page', + slug: 'static-page-draft', + mobiledoc: DataGenerator.markdownToMobiledoc('Hopefully you don\'t find it a bore.
'), page: 1, - status: "draft" + status: 'draft' }, { id: ObjectId.generate(), - title: "This is a scheduled post!!", - slug: "scheduled-post", - mobiledoc: DataGenerator.markdownToMobiledoc("