mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Fixed missing defaults in model layer
no issue - reported in the forum: https://forum.ghost.org/t/publishing-with-a-single-post-request-to-posts/1648 - the defaults are defined in two places 1. on the schema level (defaults for the database) 2. on the ORM (model layer) - the defaults on the db layer are set correctly when inserting a new resource - but if we don't apply all defaults on the model layer, it will happen that model events are emitted without the correct defaults - see comment in code base - it's caused by the fact that knex only returns the inserted resource id (probably caused by the fact knex has to support x databases) - components/modules are listening on model events and expect: 1. a complete set of attributes 2. a complete set of defaults 3. sanitized values e.g. bool, date - this commit fixes: 1. added missing defaults for user & post model 2. sanitize booleans (0|1 => false|true) 3. added tests to ensure this works as expected 4. clarfies the usage of `defaults` Regarding https://forum.ghost.org/t/publishing-with-a-single-post-request-to-posts/1648: - the post event was emitted with the following values {page: undefined, featured: undefined} - the urlservice receives this event and won't match the resource against collection filters correctly - NOTE: the post data in the db were correct
This commit is contained in:
parent
61db6defde
commit
00cf043e15
6 changed files with 122 additions and 25 deletions
|
@ -220,6 +220,11 @@ validateSchema = function validateSchema(tableName, model, options) {
|
||||||
context: tableName + '.' + columnKey
|
context: tableName + '.' + columnKey
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CASE: ensure we transform 0|1 to false|true
|
||||||
|
if (!validator.empty(strVal)) {
|
||||||
|
model.set(columnKey, !!model.get(columnKey));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: check if mandatory values should be enforced
|
// TODO: check if mandatory values should be enforced
|
||||||
|
|
|
@ -21,18 +21,31 @@ Post = ghostBookshelf.Model.extend({
|
||||||
tableName: 'posts',
|
tableName: 'posts',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ## NOTE:
|
* @NOTE
|
||||||
* We define the defaults on the schema (db) and model level.
|
|
||||||
* When inserting resources, the defaults are available **after** calling `.save`.
|
|
||||||
* But they are available when the model hooks are triggered (e.g. onSaving).
|
|
||||||
* It might be useful to keep them in the model layer for any connected logic.
|
|
||||||
*
|
*
|
||||||
* e.g. if `model.get('status') === draft; do something;
|
* We define the defaults on the schema (db) and model level.
|
||||||
|
*
|
||||||
|
* Why?
|
||||||
|
* - when you insert a resource, Knex does only return the id of the created resource
|
||||||
|
* - see https://knexjs.org/#Builder-insert
|
||||||
|
* - that means `defaultTo` is a pure database configuration (!)
|
||||||
|
* - Bookshelf just returns the model values which you have asked Bookshelf to insert
|
||||||
|
* - it can't return the `defaultTo` value from the schema/db level
|
||||||
|
* - but the db defaults defined in the schema are saved in the database correctly
|
||||||
|
* - `models.Post.add` always does to operations:
|
||||||
|
* 1. add
|
||||||
|
* 2. fetch (this ensures we fetch the whole resource from the database)
|
||||||
|
* - that means we have to apply the defaults on the model layer to ensure a complete field set
|
||||||
|
* 1. any connected logic in our model hooks e.g. beforeSave
|
||||||
|
* 2. model events e.g. "post.published" are using the inserted resource, not the fetched resource
|
||||||
*/
|
*/
|
||||||
defaults: function defaults() {
|
defaults: function defaults() {
|
||||||
return {
|
return {
|
||||||
uuid: uuid.v4(),
|
uuid: uuid.v4(),
|
||||||
status: 'draft'
|
status: 'draft',
|
||||||
|
featured: false,
|
||||||
|
page: false,
|
||||||
|
visibility: 'public'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,9 @@ User = ghostBookshelf.Model.extend({
|
||||||
|
|
||||||
defaults: function defaults() {
|
defaults: function defaults() {
|
||||||
return {
|
return {
|
||||||
password: security.identifier.uid(50)
|
password: security.identifier.uid(50),
|
||||||
|
visibility: 'public',
|
||||||
|
status: 'active'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,30 @@ describe('Validation', function () {
|
||||||
{method: 'insert'}
|
{method: 'insert'}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('transforms 0 and 1', function () {
|
||||||
|
const post = models.Post.forge(testUtils.DataGenerator.forKnex.createPost({slug: 'test', featured: 0, page: 1}));
|
||||||
|
post.get('featured').should.eql(0);
|
||||||
|
post.get('page').should.eql(1);
|
||||||
|
|
||||||
|
return validation.validateSchema('posts', post, {method: 'insert'})
|
||||||
|
.then(function () {
|
||||||
|
post.get('featured').should.eql(false);
|
||||||
|
post.get('page').should.eql(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps true or false', function () {
|
||||||
|
const post = models.Post.forge(testUtils.DataGenerator.forKnex.createPost({slug: 'test', featured: true, page: false}));
|
||||||
|
post.get('featured').should.eql(true);
|
||||||
|
post.get('page').should.eql(false);
|
||||||
|
|
||||||
|
return validation.validateSchema('posts', post, {method: 'insert'})
|
||||||
|
.then(function () {
|
||||||
|
post.get('featured').should.eql(true);
|
||||||
|
post.get('page').should.eql(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('models.edit', function () {
|
describe('models.edit', function () {
|
||||||
|
|
|
@ -56,6 +56,10 @@ describe('Unit: models/post', function () {
|
||||||
|
|
||||||
_.each(_.keys(_.omit(schema.tables.posts, ['mobiledoc', 'amp', 'plaintext'])), (key) => {
|
_.each(_.keys(_.omit(schema.tables.posts, ['mobiledoc', 'amp', 'plaintext'])), (key) => {
|
||||||
should.exist(post.hasOwnProperty(key));
|
should.exist(post.hasOwnProperty(key));
|
||||||
|
|
||||||
|
if (['page', 'status', 'visibility', 'featured'].indexOf(key) !== -1) {
|
||||||
|
events.post[0].data[key].should.eql(schema.tables.posts[key].defaultTo);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
should.not.exist(post.authors);
|
should.not.exist(post.authors);
|
||||||
|
@ -67,6 +71,10 @@ describe('Unit: models/post', function () {
|
||||||
|
|
||||||
_.each(_.keys(_.omit(schema.tables.posts, ['mobiledoc', 'amp', 'plaintext'])), (key) => {
|
_.each(_.keys(_.omit(schema.tables.posts, ['mobiledoc', 'amp', 'plaintext'])), (key) => {
|
||||||
should.exist(events.post[0].data.hasOwnProperty(key));
|
should.exist(events.post[0].data.hasOwnProperty(key));
|
||||||
|
|
||||||
|
if (['page', 'status', 'visibility', 'featured'].indexOf(key) !== -1) {
|
||||||
|
events.post[0].data[key].should.eql(schema.tables.posts[key].defaultTo);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
should.exist(events.post[0].data.authors);
|
should.exist(events.post[0].data.authors);
|
||||||
|
@ -76,6 +84,29 @@ describe('Unit: models/post', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('with page:1', function () {
|
||||||
|
const events = {
|
||||||
|
post: []
|
||||||
|
};
|
||||||
|
|
||||||
|
sandbox.stub(models.Post.prototype, 'emitChange').callsFake(function (event) {
|
||||||
|
events.post.push({event: event, data: this.toJSON()});
|
||||||
|
});
|
||||||
|
|
||||||
|
return models.Post.add({
|
||||||
|
title: 'My beautiful title.',
|
||||||
|
page: 1
|
||||||
|
}, testUtils.context.editor)
|
||||||
|
.then((post) => {
|
||||||
|
post.get('title').should.eql('My beautiful title.');
|
||||||
|
post = post.toJSON();
|
||||||
|
|
||||||
|
// transformed 1 to true
|
||||||
|
post.page.should.eql(true);
|
||||||
|
events.post[0].data.page.should.eql(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('use `withRelated=tags`', function () {
|
it('use `withRelated=tags`', function () {
|
||||||
const events = {
|
const events = {
|
||||||
post: []
|
post: []
|
||||||
|
@ -97,21 +128,12 @@ describe('Unit: models/post', function () {
|
||||||
post.get('title').should.eql('My beautiful title.');
|
post.get('title').should.eql('My beautiful title.');
|
||||||
post = post.toJSON();
|
post = post.toJSON();
|
||||||
|
|
||||||
_.each(_.keys(_.omit(schema.tables.posts, ['mobiledoc', 'amp', 'plaintext'])), (key) => {
|
|
||||||
should.exist(post.hasOwnProperty(key));
|
|
||||||
});
|
|
||||||
|
|
||||||
should.not.exist(post.authors);
|
should.not.exist(post.authors);
|
||||||
should.not.exist(post.primary_author);
|
should.not.exist(post.primary_author);
|
||||||
should.exist(post.tags);
|
should.exist(post.tags);
|
||||||
should.exist(post.primary_tag);
|
should.exist(post.primary_tag);
|
||||||
|
|
||||||
events.post[0].event.should.eql('added');
|
events.post[0].event.should.eql('added');
|
||||||
|
|
||||||
_.each(_.keys(_.omit(schema.tables.posts, ['mobiledoc', 'amp', 'plaintext'])), (key) => {
|
|
||||||
should.exist(events.post[0].data.hasOwnProperty(key));
|
|
||||||
});
|
|
||||||
|
|
||||||
should.exist(events.post[0].data.authors);
|
should.exist(events.post[0].data.authors);
|
||||||
should.exist(events.post[0].data.primary_author);
|
should.exist(events.post[0].data.primary_author);
|
||||||
should.exist(events.post[0].data.tags);
|
should.exist(events.post[0].data.tags);
|
||||||
|
@ -140,10 +162,6 @@ describe('Unit: models/post', function () {
|
||||||
post.get('title').should.eql('My beautiful title.');
|
post.get('title').should.eql('My beautiful title.');
|
||||||
post = post.toJSON();
|
post = post.toJSON();
|
||||||
|
|
||||||
_.each(_.keys(_.omit(schema.tables.posts, ['mobiledoc', 'amp', 'plaintext'])), (key) => {
|
|
||||||
should.exist(post.hasOwnProperty(key));
|
|
||||||
});
|
|
||||||
|
|
||||||
should.exist(post.authors);
|
should.exist(post.authors);
|
||||||
should.exist(post.primary_author);
|
should.exist(post.primary_author);
|
||||||
should.exist(post.tags);
|
should.exist(post.tags);
|
||||||
|
@ -151,10 +169,6 @@ describe('Unit: models/post', function () {
|
||||||
|
|
||||||
events.post[0].event.should.eql('added');
|
events.post[0].event.should.eql('added');
|
||||||
|
|
||||||
_.each(_.keys(_.omit(schema.tables.posts, ['mobiledoc', 'amp', 'plaintext'])), (key) => {
|
|
||||||
should.exist(events.post[0].data.hasOwnProperty(key));
|
|
||||||
});
|
|
||||||
|
|
||||||
should.exist(events.post[0].data.authors);
|
should.exist(events.post[0].data.authors);
|
||||||
should.exist(events.post[0].data.primary_author);
|
should.exist(events.post[0].data.primary_author);
|
||||||
should.exist(events.post[0].data.tags);
|
should.exist(events.post[0].data.tags);
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
const should = require('should'),
|
const should = require('should'),
|
||||||
sinon = require('sinon'),
|
sinon = require('sinon'),
|
||||||
|
_ = require('lodash'),
|
||||||
|
schema = require('../../../server/data/schema'),
|
||||||
models = require('../../../server/models'),
|
models = require('../../../server/models'),
|
||||||
validation = require('../../../server/data/validation'),
|
validation = require('../../../server/data/validation'),
|
||||||
common = require('../../../server/lib/common'),
|
common = require('../../../server/lib/common'),
|
||||||
|
@ -334,4 +336,41 @@ describe('Unit: models/user', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Add', function () {
|
||||||
|
const events = {
|
||||||
|
user: []
|
||||||
|
};
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
models.init();
|
||||||
|
|
||||||
|
sandbox.stub(models.User.prototype, 'emitChange').callsFake(function (event) {
|
||||||
|
events.user.push({event: event, data: this.toJSON()});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function () {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults', function () {
|
||||||
|
return models.User.add({slug: 'joe', name: 'Joe', email: 'joe@test.com'})
|
||||||
|
.then(function (user) {
|
||||||
|
user.get('name').should.eql('Joe');
|
||||||
|
user.get('email').should.eql('joe@test.com');
|
||||||
|
user.get('slug').should.eql('joe');
|
||||||
|
user.get('visibility').should.eql('public');
|
||||||
|
user.get('status').should.eql('active');
|
||||||
|
|
||||||
|
_.each(_.keys(schema.tables.users), (key) => {
|
||||||
|
should.exist(events.user[0].data.hasOwnProperty(key));
|
||||||
|
|
||||||
|
if (['status', 'visibility'].indexOf(key) !== -1) {
|
||||||
|
events.user[0].data[key].should.eql(schema.tables.users[key].defaultTo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue