2014-04-27 20:58:34 +04:00
|
|
|
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'),
|
2014-02-19 17:57:26 +04:00
|
|
|
|
|
|
|
ghostBookshelf;
|
2013-06-25 15:43:15 +04:00
|
|
|
|
2013-09-23 02:20:08 +04:00
|
|
|
// Initializes a new Bookshelf instance, for reference elsewhere in Ghost.
|
2013-11-26 07:22:59 +04:00
|
|
|
ghostBookshelf = Bookshelf.ghost = Bookshelf.initialize(config().database);
|
|
|
|
ghostBookshelf.client = config().database.client;
|
2013-06-25 15:43:15 +04:00
|
|
|
|
|
|
|
// The Base Model which other Ghost objects will inherit from,
|
|
|
|
// including some convenience functions as static properties on the model.
|
2013-09-23 02:20:08 +04:00
|
|
|
ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
2013-06-25 15:43:15 +04:00
|
|
|
|
2013-09-14 23:01:46 +04:00
|
|
|
hasTimestamps: true,
|
|
|
|
|
2014-02-19 17:57:26 +04:00
|
|
|
// get permitted attributs from schema.js
|
|
|
|
permittedAttributes: function () {
|
|
|
|
return _.keys(schema.tables[this.tableName]);
|
|
|
|
},
|
|
|
|
|
2013-09-14 23:01:46 +04:00
|
|
|
defaults: function () {
|
|
|
|
return {
|
|
|
|
uuid: uuid.v4()
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
initialize: function () {
|
2014-04-27 20:58:34 +04:00
|
|
|
var self = this,
|
|
|
|
options = arguments[1] || {};
|
|
|
|
|
|
|
|
// make options include available for toJSON()
|
|
|
|
if (options.include) {
|
|
|
|
this.include = _.clone(options.include);
|
|
|
|
}
|
|
|
|
|
2013-09-14 23:01:46 +04:00
|
|
|
this.on('creating', this.creating, this);
|
2014-02-19 17:57:26 +04:00
|
|
|
this.on('saving', function (model, attributes, options) {
|
|
|
|
return when(self.saving(model, attributes, options)).then(function () {
|
|
|
|
return self.validate(model, attributes, options);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
validate: function () {
|
2014-05-05 17:51:21 +04:00
|
|
|
return validation.validateSchema(this.tableName, this.toJSON());
|
2013-09-14 23:01:46 +04:00
|
|
|
},
|
|
|
|
|
2014-04-03 17:03:09 +04:00
|
|
|
creating: function (newObj, attr, options) {
|
2013-09-14 23:01:46 +04:00
|
|
|
if (!this.get('created_by')) {
|
2014-04-03 17:03:09 +04:00
|
|
|
this.set('created_by', options.user);
|
2013-09-14 23:01:46 +04:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2014-04-03 17:03:09 +04:00
|
|
|
saving: function (newObj, attr, options) {
|
|
|
|
// Remove any properties which don't belong on the model
|
2014-02-19 17:57:26 +04:00
|
|
|
this.attributes = this.pick(this.permittedAttributes());
|
2014-04-22 05:04:30 +04:00
|
|
|
// Store the previous attributes so we can tell what was updated later
|
|
|
|
this._updatedAttributes = newObj.previousAttributes();
|
|
|
|
|
2014-04-03 17:03:09 +04:00
|
|
|
this.set('updated_by', options.user);
|
2013-09-14 23:01:46 +04:00
|
|
|
},
|
|
|
|
|
2013-06-25 15:43:15 +04:00
|
|
|
// Base prototype properties will go here
|
2013-07-08 15:39:11 +04:00
|
|
|
// Fix problems with dates
|
|
|
|
fixDates: function (attrs) {
|
|
|
|
_.each(attrs, function (value, key) {
|
|
|
|
if (key.substr(-3) === '_at' && value !== null) {
|
2013-08-16 18:11:36 +04:00
|
|
|
attrs[key] = moment(attrs[key]).toDate();
|
2013-07-08 15:39:11 +04:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return attrs;
|
|
|
|
},
|
|
|
|
|
2014-04-25 11:55:53 +04:00
|
|
|
// Convert integers to real booleans
|
2014-01-05 02:16:29 +04:00
|
|
|
fixBools: function (attrs) {
|
2014-04-25 11:55:53 +04:00
|
|
|
var self = this;
|
2014-01-05 02:16:29 +04:00
|
|
|
_.each(attrs, function (value, key) {
|
2014-04-25 11:55:53 +04:00
|
|
|
if (schema.tables[self.tableName][key].type === "bool") {
|
|
|
|
attrs[key] = value ? true : false;
|
2014-01-05 02:16:29 +04:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return attrs;
|
|
|
|
},
|
|
|
|
|
2014-04-25 11:55:53 +04:00
|
|
|
// format date before writing to DB, bools work
|
2013-07-08 15:39:11 +04:00
|
|
|
format: function (attrs) {
|
2014-04-25 11:55:53 +04:00
|
|
|
return this.fixDates(attrs);
|
|
|
|
},
|
|
|
|
|
|
|
|
// format data and bool when fetching from DB
|
|
|
|
parse: function (attrs) {
|
2014-01-05 02:16:29 +04:00
|
|
|
return this.fixBools(this.fixDates(attrs));
|
2013-07-08 15:39:11 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
toJSON: function (options) {
|
2014-04-25 11:55:53 +04:00
|
|
|
var attrs = _.extend({}, this.attributes),
|
2014-04-27 20:58:34 +04:00
|
|
|
self = this;
|
2013-07-08 15:39:11 +04:00
|
|
|
|
|
|
|
if (options && options.shallow) {
|
|
|
|
return attrs;
|
|
|
|
}
|
|
|
|
|
2014-04-27 20:58:34 +04:00
|
|
|
if (options && options.idOnly) {
|
|
|
|
return attrs.id;
|
|
|
|
}
|
|
|
|
|
|
|
|
_.each(this.relations, function (relation, key) {
|
2013-09-24 14:46:30 +04:00
|
|
|
if (key.substring(0, 7) !== '_pivot_') {
|
2014-04-27 20:58:34 +04:00
|
|
|
// 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});
|
|
|
|
}
|
2013-08-21 16:55:58 +04:00
|
|
|
}
|
2013-07-08 15:39:11 +04:00
|
|
|
});
|
|
|
|
|
|
|
|
return attrs;
|
2013-09-14 23:01:46 +04:00
|
|
|
},
|
|
|
|
|
2013-10-09 22:11:29 +04:00
|
|
|
sanitize: function (attr) {
|
|
|
|
return sanitize(this.get(attr)).xss();
|
2014-04-22 05:04:30 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
// Get attributes that have been updated (values before a .save() call)
|
|
|
|
updatedAttributes: function () {
|
|
|
|
return this._updatedAttributes || {};
|
|
|
|
},
|
|
|
|
|
|
|
|
// Get a specific updated attribute value
|
|
|
|
updated: function (attr) {
|
|
|
|
return this.updatedAttributes()[attr];
|
2013-07-08 15:39:11 +04:00
|
|
|
}
|
2013-06-25 15:43:15 +04:00
|
|
|
|
|
|
|
}, {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Naive find all
|
2014-04-21 22:04:20 +04:00
|
|
|
* @param {Object} options (optional)
|
2013-06-25 15:43:15 +04:00
|
|
|
*/
|
|
|
|
findAll: function (options) {
|
|
|
|
options = options || {};
|
2014-04-27 20:58:34 +04:00
|
|
|
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;
|
|
|
|
});
|
2013-06-25 15:43:15 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
browse: function () {
|
|
|
|
return this.findAll.apply(this, arguments);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Naive find one where args match
|
2014-04-21 22:04:20 +04:00
|
|
|
* @param {Object} args
|
|
|
|
* @param {Object} options (optional)
|
2013-06-25 15:43:15 +04:00
|
|
|
*/
|
|
|
|
findOne: function (args, options) {
|
|
|
|
options = options || {};
|
2014-04-27 20:58:34 +04:00
|
|
|
return this.forge(args, {include: options.include}).fetch(options);
|
2013-06-25 15:43:15 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
read: function () {
|
|
|
|
return this.findOne.apply(this, arguments);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Naive edit
|
2014-04-21 22:04:20 +04:00
|
|
|
* @param {Object} editedObj
|
|
|
|
* @param {Object} options (optional)
|
2013-06-25 15:43:15 +04:00
|
|
|
*/
|
|
|
|
edit: function (editedObj, options) {
|
|
|
|
options = options || {};
|
|
|
|
return this.forge({id: editedObj.id}).fetch(options).then(function (foundObj) {
|
2014-04-16 14:09:03 +04:00
|
|
|
if (foundObj) {
|
|
|
|
return foundObj.save(editedObj, options);
|
|
|
|
}
|
2013-06-25 15:43:15 +04:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
update: function () {
|
|
|
|
return this.edit.apply(this, arguments);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Naive create
|
2014-04-21 22:04:20 +04:00
|
|
|
* @param {Object} newObj
|
|
|
|
* @param {Object} options (optional)
|
2013-06-25 15:43:15 +04:00
|
|
|
*/
|
|
|
|
add: function (newObj, options) {
|
|
|
|
options = options || {};
|
2013-12-26 07:48:16 +04:00
|
|
|
var instance = this.forge(newObj);
|
|
|
|
// We allow you to disable timestamps
|
|
|
|
// when importing posts so that
|
|
|
|
// the new posts `updated_at` value
|
|
|
|
// is the same as the import json blob.
|
|
|
|
// More details refer to https://github.com/TryGhost/Ghost/issues/1696
|
|
|
|
if (options.importing) {
|
|
|
|
instance.hasTimestamps = false;
|
|
|
|
}
|
|
|
|
return instance.save(null, options);
|
2013-06-25 15:43:15 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
create: function () {
|
|
|
|
return this.add.apply(this, arguments);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Naive destroy
|
2014-04-21 22:04:20 +04:00
|
|
|
* @param {Object} _identifier
|
|
|
|
* @param {Object} options (optional)
|
2013-06-25 15:43:15 +04:00
|
|
|
*/
|
|
|
|
destroy: function (_identifier, options) {
|
|
|
|
options = options || {};
|
|
|
|
return this.forge({id: _identifier}).destroy(options);
|
|
|
|
},
|
|
|
|
|
|
|
|
'delete': function () {
|
|
|
|
return this.destroy.apply(this, arguments);
|
2013-12-20 17:36:00 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
// #### generateSlug
|
|
|
|
// Create a string act as the permalink for an object.
|
|
|
|
generateSlug: function (Model, base, readOptions) {
|
|
|
|
var slug,
|
|
|
|
slugTryCount = 1,
|
2014-03-23 22:52:25 +04:00
|
|
|
baseName = Model.prototype.tableName.replace(/s$/, ''),
|
2014-01-05 21:55:59 +04:00
|
|
|
// Look for a post with a matching slug, append an incrementing number if so
|
2014-03-23 22:52:25 +04:00
|
|
|
checkIfSlugExists;
|
2013-12-20 17:36:00 +04:00
|
|
|
|
2014-03-23 22:52:25 +04:00
|
|
|
checkIfSlugExists = function (slugToFind) {
|
|
|
|
var args = {slug: slugToFind};
|
|
|
|
//status is needed for posts
|
|
|
|
if (readOptions && readOptions.status) {
|
|
|
|
args.status = readOptions.status;
|
|
|
|
}
|
|
|
|
return Model.findOne(args, readOptions).then(function (found) {
|
|
|
|
var trimSpace;
|
2013-12-20 17:36:00 +04:00
|
|
|
|
2014-03-23 22:52:25 +04:00
|
|
|
if (!found) {
|
|
|
|
return when.resolve(slugToFind);
|
|
|
|
}
|
2013-12-20 17:36:00 +04:00
|
|
|
|
2014-03-23 22:52:25 +04:00
|
|
|
slugTryCount += 1;
|
2013-12-20 17:36:00 +04:00
|
|
|
|
2014-03-23 22:52:25 +04:00
|
|
|
// If this is the first time through, add the hyphen
|
|
|
|
if (slugTryCount === 2) {
|
|
|
|
slugToFind += '-';
|
|
|
|
} else {
|
|
|
|
// Otherwise, trim the number off the end
|
|
|
|
trimSpace = -(String(slugTryCount - 1).length);
|
|
|
|
slugToFind = slugToFind.slice(0, trimSpace);
|
|
|
|
}
|
2013-12-20 17:36:00 +04:00
|
|
|
|
2014-03-23 22:52:25 +04:00
|
|
|
slugToFind += slugTryCount;
|
|
|
|
|
|
|
|
return checkIfSlugExists(slugToFind);
|
|
|
|
});
|
|
|
|
};
|
2013-12-20 17:36:00 +04:00
|
|
|
|
2014-01-26 08:32:08 +04:00
|
|
|
slug = base.trim();
|
|
|
|
|
|
|
|
// Remove non ascii characters
|
|
|
|
slug = unidecode(slug);
|
|
|
|
|
2013-12-20 17:36:00 +04:00
|
|
|
// Remove URL reserved chars: `:/?#[]@!$&'()*+,;=` as well as `\%<>|^~£"`
|
2014-01-26 08:32:08 +04:00
|
|
|
slug = slug.replace(/[:\/\?#\[\]@!$&'()*+,;=\\%<>\|\^~£"]/g, '')
|
2013-12-20 17:36:00 +04:00
|
|
|
// Replace dots and spaces with a dash
|
|
|
|
.replace(/(\s|\.)/g, '-')
|
|
|
|
// Convert 2 or more dashes into a single dash
|
|
|
|
.replace(/-+/g, '-')
|
|
|
|
// Make the whole thing lowercase
|
|
|
|
.toLowerCase();
|
|
|
|
|
|
|
|
// Remove trailing hyphen
|
|
|
|
slug = slug.charAt(slug.length - 1) === '-' ? slug.substr(0, slug.length - 1) : slug;
|
2014-01-26 08:32:08 +04:00
|
|
|
|
2013-12-20 17:36:00 +04:00
|
|
|
// Check the filtered slug doesn't match any of the reserved keywords
|
2014-04-24 01:19:44 +04:00
|
|
|
slug = /^(ghost|ghost\-admin|admin|wp\-admin|wp\-login|dashboard|logout|login|signin|signup|signout|register|archive|archives|category|categories|tag|tags|page|pages|post|posts|public|user|users|rss|feed)$/g
|
2014-03-23 22:52:25 +04:00
|
|
|
.test(slug) ? slug + '-' + baseName : slug;
|
2013-12-20 17:36:00 +04:00
|
|
|
|
|
|
|
//if slug is empty after trimming use "post"
|
|
|
|
if (!slug) {
|
2014-03-23 22:52:25 +04:00
|
|
|
slug = baseName;
|
2013-12-20 17:36:00 +04:00
|
|
|
}
|
|
|
|
// Test for duplicate slugs.
|
|
|
|
return checkIfSlugExists(slug);
|
2013-06-25 15:43:15 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
2013-09-23 02:20:08 +04:00
|
|
|
module.exports = ghostBookshelf;
|