0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-08 02:52:39 -05:00

Set migrations to use new 000 schema

issue #632

- removed old schemas
- updated base model to reflect all of the consistent behaviours and properties across the models
- updated all models to match the new schema

TODO

- no fixtures are currently loaded except settings
- need to rename properties across the codebase
This commit is contained in:
Hannah Wolfe 2013-09-14 20:01:46 +01:00
parent 72229fa8ea
commit d587a845d4
11 changed files with 154 additions and 444 deletions

View file

@ -1,128 +0,0 @@
var when = require('when'),
knex = require('../../models/base').Knex,
fixtures = require('../fixtures/001'),
up,
down;
up = function () {
return when.all([
knex.Schema.createTable('posts', function (t) {
t.increments().primary();
t.string('uuid');
t.string('title');
t.string('slug');
t.text('content_raw');
t.text('content');
t.string('meta_title').nullable();
t.string('meta_description').nullable();
t.string('meta_keywords').nullable();
t.bool('featured').defaultTo(false);
t.string('image').nullable();
t.string('status');
t.string('language').defaultTo('en');
t.integer('author_id');
t.dateTime('created_at');
t.integer('created_by');
t.dateTime('updated_at').nullable();
t.integer('updated_by').nullable();
t.dateTime('published_at').nullable();
t.integer('published_by').nullable();
}),
knex.Schema.createTable('users', function (t) {
t.increments().primary();
t.string('uuid');
t.string('full_name');
t.string('password');
t.string('email_address');
t.string('profile_picture');
t.string('cover_picture');
t.text('bio');
t.string('url');
t.dateTime('created_at');
t.integer('created_by');
t.dateTime('updated_at');
t.integer('updated_by');
}),
knex.Schema.createTable('roles', function (t) {
t.increments().primary();
t.string('name');
t.string('description');
}),
knex.Schema.createTable('roles_users', function (t) {
t.increments().primary();
t.integer('role_id');
t.integer('user_id');
}),
knex.Schema.createTable('permissions', function (t) {
t.increments().primary();
t.string('name');
t.string('object_type');
t.string('action_type');
t.integer('object_id');
}),
knex.Schema.createTable('permissions_users', function (t) {
t.increments().primary();
t.integer('user_id');
t.integer('permission_id');
}),
knex.Schema.createTable('permissions_roles', function (t) {
t.increments().primary();
t.integer('role_id');
t.integer('permission_id');
}),
knex.Schema.createTable('settings', function (t) {
t.increments().primary();
t.string('uuid');
t.string('key').unique();
t.text('value');
t.string('type');
t.dateTime('created_at');
t.integer('created_by');
t.dateTime('updated_at');
t.integer('updated_by');
})
// Once we create all of the initial tables, bootstrap any of the data
]).then(function () {
return when.all([
knex('posts').insert(fixtures.posts),
// knex('users').insert(fixtures.users),
knex('roles').insert(fixtures.roles),
// knex('roles_users').insert(fixtures.roles_users),
knex('permissions').insert(fixtures.permissions),
knex('permissions_roles').insert(fixtures.permissions_roles),
knex('settings').insert({ key: 'currentVersion', 'value': '001', type: 'core' })
]);
});
};
down = function () {
return when.all([
knex.Schema.dropTableIfExists("posts"),
knex.Schema.dropTableIfExists("users"),
knex.Schema.dropTableIfExists("roles"),
knex.Schema.dropTableIfExists("settings"),
knex.Schema.dropTableIfExists("permissions")
]).then(function () {
// Drop the relation tables after the model tables?
return when.all([
knex.Schema.dropTableIfExists("roles_users"),
knex.Schema.dropTableIfExists("permissions_users"),
knex.Schema.dropTableIfExists("permissions_roles")
]);
});
};
exports.up = up;
exports.down = down;

View file

@ -1,83 +0,0 @@
var when = require('when'),
knex = require('../../models/base').Knex,
migrationVersion = '002',
fixtures = require('../fixtures/' + migrationVersion),
errors = require('../../errorHandling'),
up,
down;
up = function () {
return when.all([
knex.Schema.createTable('tags', function (t) {
t.increments().primary();
t.string('uuid');
t.string('name');
t.string('slug');
t.text('descripton');
t.integer('parent_id').nullable();
t.string('meta_title');
t.text('meta_description');
t.string('meta_keywords');
t.dateTime('created_at');
t.integer('created_by');
t.dateTime('updated_at').nullable();
t.integer('updated_by').nullable();
}),
knex.Schema.createTable('posts_tags', function (t) {
t.increments().primary();
t.string('uuid');
t.integer('post_id');
t.integer('tag_id');
}),
knex.Schema.createTable('custom_data', function (t) {
t.increments().primary();
t.string('uuid');
t.string('name');
t.string('slug');
t.text('value');
t.string('type').defaultTo('html');
t.string('owner').defaultTo('Ghost');
t.string('meta_title');
t.text('meta_description');
t.string('meta_keywords');
t.dateTime('created_at');
t.integer('created_by');
t.dateTime('updated_at').nullable();
t.integer('updated_by').nullable();
}),
knex.Schema.createTable('posts_custom_data', function (t) {
t.increments().primary();
t.string('uuid');
t.integer('post_id');
t.integer('custom_data_id');
}),
knex.Schema.table('users', function (t) {
t.string('location').after('bio');
})
]).then(function () {
// Lastly, update the current version settings to reflect this version
return knex('settings')
.where('key', 'currentVersion')
.update({ 'value': migrationVersion });
});
};
down = function () {
return when.all([
knex.Schema.dropTableIfExists("tags"),
knex.Schema.dropTableIfExists("custom_data")
]).then(function () {
// Drop the relation tables after the model tables?
return when.all([
knex.Schema.dropTableIfExists("posts_tags"),
knex.Schema.dropTableIfExists("posts_custom_data")
]);
});
// Should we also drop the currentVersion?
};
exports.up = up;
exports.down = down;

View file

@ -1,49 +0,0 @@
var when = require('when'),
_ = require('underscore'),
knex = require('../../models/base').Knex,
migrationVersion = '003',
fixtures = require('../fixtures/' + migrationVersion),
errors = require('../../errorHandling'),
up,
down;
up = function up() {
return when.all([
knex('posts')
.whereNull('language')
.orWhere('language', 'en')
.update({
'language': 'en_US'
}),
knex('posts')
.whereNull('featured')
.update({
'featured': false
})
]).then(function incrementVersion() {
// Lastly, update the current version settings to reflect this version
return knex('settings')
.where('key', 'currentVersion')
.update({ 'value': migrationVersion });
});
};
down = function down() {
return when.all([
// No new tables as of yet, so just return a wrapped value
when(true)
]);
};
exports.up = up;
exports.down = down;

View file

@ -4,11 +4,11 @@ var _ = require('underscore'),
series = require('when/sequence'),
errors = require('../../errorHandling'),
knex = require('../../models/base').Knex,
initialVersion = '001',
initialVersion = '000',
// This currentVersion string should always be the current version of Ghost,
// we could probably load it from the config file.
// - Will be possible after default-settings.json restructure
currentVersion = '003';
currentVersion = '000';
function getCurrentVersion() {
return knex.Schema.hasTable('settings').then(function () {

View file

@ -1,7 +1,9 @@
var GhostBookshelf,
Bookshelf = require('bookshelf'),
when = require('when'),
moment = require('moment'),
_ = require('underscore'),
uuid = require('node-uuid'),
config = require('../../../config'),
Validator = require('validator').Validator;
@ -15,6 +17,33 @@ GhostBookshelf.validator = new Validator();
// including some convenience functions as static properties on the model.
GhostBookshelf.Model = GhostBookshelf.Model.extend({
hasTimestamps: true,
defaults: function () {
return {
uuid: uuid.v4()
};
},
initialize: function () {
this.on('creating', this.creating, this);
this.on('saving', this.saving, this);
this.on('saving', this.validate, this);
},
creating: function () {
if (!this.get('created_by')) {
this.set('created_by', 1);
}
},
saving: function () {
// Remove any properties which don't belong on the post model
this.attributes = this.pick(this.permittedAttributes);
this.set('updated_by', 1);
},
// Base prototype properties will go here
// Fix problems with dates
fixDates: function (attrs) {
@ -46,6 +75,60 @@ GhostBookshelf.Model = GhostBookshelf.Model.extend({
});
return attrs;
},
// #### generateSlug
// Create a string act as the permalink for an object.
generateSlug: function (Model, base) {
var slug,
slugTryCount = 1,
// Look for a post with a matching slug, append an incrementing number if so
checkIfSlugExists = function (slugToFind) {
return Model.read({slug: slugToFind}).then(function (found) {
var trimSpace;
if (!found) {
return when.resolve(slugToFind);
}
slugTryCount += 1;
// 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);
}
slugToFind += slugTryCount;
return checkIfSlugExists(slugToFind);
});
};
// Remove URL reserved chars: `:/?#[]@!$&'()*+,;=` as well as `\%<>|^~£"`
slug = base.trim().replace(/[:\/\?#\[\]@!$&'()*+,;=\\%<>\|\^~£"]/g, '')
// 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 hypen
slug = slug.charAt(slug.length - 1) === '-' ? slug.substr(0, slug.length - 1) : slug;
// Check the filtered slug doesn't match any of the reserved keywords
slug = /^(ghost|ghost\-admin|admin|wp\-admin|dashboard|login|archive|archives|category|categories|tag|tags|page|pages|post|posts|user|users)$/g
.test(slug) ? slug + '-post' : slug;
//if slug is empty after trimming use "post"
if (!slug) {
slug = "post";
}
// Test for duplicate slugs.
return checkIfSlugExists(slug);
}
}, {

View file

@ -5,27 +5,18 @@ var GhostBookshelf = require('./base'),
Permissions;
Permission = GhostBookshelf.Model.extend({
tableName: 'permissions',
permittedAttributes: ['id', 'name', 'object_type', 'action_type', 'object_id'],
permittedAttributes: ['id', 'uuid', 'name', 'object_type', 'action_type', 'object_id', 'created_at', 'created_by',
'updated_at', 'updated_by'],
initialize: function () {
this.on('saving', this.saving, this);
this.on('saving', this.validate, this);
},
validate: function () {
// TODO: validate object_type, action_type and object_id
GhostBookshelf.validator.check(this.get('name'), "Permission name cannot be blank").notEmpty();
},
saving: function () {
// Deal with the related data here
// Remove any properties which don't belong on the post model
this.attributes = this.pick(this.permittedAttributes);
},
roles: function () {
return this.belongsToMany(Role);
},

View file

@ -18,18 +18,15 @@ Post = GhostBookshelf.Model.extend({
tableName: 'posts',
permittedAttributes: [
'id', 'uuid', 'title', 'slug', 'content_raw', 'content', 'meta_title', 'meta_description', 'meta_keywords',
'id', 'uuid', 'title', 'slug', 'markdown', 'html', 'meta_title', 'meta_description',
'featured', 'image', 'status', 'language', 'author_id', 'created_at', 'created_by', 'updated_at', 'updated_by',
'published_at', 'published_by'
],
hasTimestamps: true,
defaults: function () {
return {
uuid: uuid.v4(),
status: 'draft',
language: config.defaultLang
status: 'draft'
};
},
@ -47,108 +44,49 @@ Post = GhostBookshelf.Model.extend({
},
saving: function () {
// Deal with the related data here
var self = this;
// Remove any properties which don't belong on the post model
this.attributes = this.pick(this.permittedAttributes);
this.set('content', converter.makeHtml(this.get('content_raw')));
this.set('html', converter.makeHtml(this.get('markdown')));
this.set('title', this.get('title').trim());
if (this.hasChanged('slug')) {
// Pass the new slug through the generator to strip illegal characters, detect duplicates
return this.generateSlug(this.get('slug'))
.then(function (slug) {
self.set({slug: slug});
});
}
if (this.hasChanged('status') && this.get('status') === 'published') {
this.set('published_at', new Date());
// This will need to go elsewhere in the API layer.
this.set('published_by', 1);
}
this.set('updated_by', 1);
GhostBookshelf.Model.prototype.saving.call(this);
// refactoring of ghost required in order to make these details available here
},
creating: function () {
// set any dynamic default properties
var self = this;
if (!this.get('created_by')) {
this.set('created_by', 1);
}
if (!this.get('author_id')) {
this.set('author_id', 1);
}
if (!this.get('slug')) {
// Generating a slug requires a db call to look for conflicting slugs
return this.generateSlug(this.get('title'))
if (this.hasChanged('slug')) {
// Pass the new slug through the generator to strip illegal characters, detect duplicates
return this.generateSlug(Post, this.get('slug'))
.then(function (slug) {
self.set({slug: slug});
});
}
},
// #### generateSlug
// Create a string act as the permalink for a post.
generateSlug: function (title) {
var slug,
slugTryCount = 1,
// Look for a post with a matching slug, append an incrementing number if so
checkIfSlugExists = function (slugToFind) {
return Post.read({slug: slugToFind}).then(function (found) {
var trimSpace;
creating: function () {
// set any dynamic default properties
var self = this;
if (!found) {
return when.resolve(slugToFind);
}
slugTryCount += 1;
// 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);
}
slugToFind += slugTryCount;
return checkIfSlugExists(slugToFind);
});
};
// Remove URL reserved chars: `:/?#[]@!$&'()*+,;=` as well as `\%<>|^~£"`
slug = title.trim().replace(/[:\/\?#\[\]@!$&'()*+,;=\\%<>\|\^~£"]/g, '')
// 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 hypen
slug = slug.charAt(slug.length - 1) === '-' ? slug.substr(0, slug.length - 1) : slug;
// Check the filtered slug doesn't match any of the reserved keywords
slug = /^(ghost|ghost\-admin|admin|wp\-admin|dashboard|login|archive|archives|category|categories|tag|tags|page|pages|post|posts)$/g
.test(slug) ? slug + '-post' : slug;
//if slug is empty after trimming use "post"
if (!slug) {
slug = "post";
if (!this.get('author_id')) {
this.set('author_id', 1);
}
GhostBookshelf.Model.prototype.creating.call(this);
if (!this.get('slug')) {
// Generating a slug requires a db call to look for conflicting slugs
return this.generateSlug(Post, this.get('title'))
.then(function (slug) {
self.set({slug: slug});
});
}
// Test for duplicate slugs.
return checkIfSlugExists(slug);
},
updateTags: function (newTags) {

View file

@ -5,27 +5,16 @@ var User = require('./user').User,
Roles;
Role = GhostBookshelf.Model.extend({
tableName: 'roles',
permittedAttributes: ['id', 'name', 'description'],
initialize: function () {
this.on('saving', this.saving, this);
this.on('saving', this.validate, this);
},
permittedAttributes: ['id', 'uuid', 'name', 'description', 'created_at', 'created_by', 'updated_at', 'updated_by'],
validate: function () {
GhostBookshelf.validator.check(this.get('name'), "Role name cannot be blank").notEmpty();
GhostBookshelf.validator.check(this.get('description'), "Role description cannot be blank").notEmpty();
},
saving: function () {
// Deal with the related data here
// Remove any properties which don't belong on the post model
this.attributes = this.pick(this.permittedAttributes);
},
users: function () {
return this.belongsToMany(User);
},

View file

@ -33,8 +33,6 @@ Settings = GhostBookshelf.Model.extend({
tableName: 'settings',
hasTimestamps: true,
permittedAttributes: ['id', 'uuid', 'key', 'value', 'type', 'created_at', 'created_by', 'updated_at', 'update_by'],
defaults: function () {
@ -44,10 +42,6 @@ Settings = GhostBookshelf.Model.extend({
};
},
initialize: function () {
this.on('saving', this.saving, this);
this.on('saving', this.validate, this);
},
// Validate default settings using the validator module.
// Each validation's key is a name and its value is an array of options
@ -79,13 +73,6 @@ Settings = GhostBookshelf.Model.extend({
validation[validationName].apply(validation, validationOptions);
}, this);
}
},
saving: function () {
// Deal with the related data here
// Remove any properties which don't belong on the model
this.attributes = this.pick(this.permittedAttributes);
}
}, {
read: function (_key) {

View file

@ -1,18 +1,34 @@
var Tag,
Tags,
uuid = require('node-uuid'),
Posts = require('./post').Posts,
GhostBookshelf = require('./base');
Tag = GhostBookshelf.Model.extend({
tableName: 'tags',
hasTimestamps: true,
permittedAttributes: [
'id', 'uuid', 'name', 'slug', 'description', 'parent_id', 'meta_title', 'meta_description', 'created_at',
'created_by', 'updated_at', 'updated_by'
],
defaults: function () {
return {
uuid: uuid.v4()
};
validate: function () {
return true;
},
creating: function () {
var self = this;
GhostBookshelf.Model.prototype.creating.call(this);
if (!this.get('slug')) {
// Generating a slug requires a db call to look for conflicting slugs
return this.generateSlug(Tag, this.get('name'))
.then(function (slug) {
self.set({slug: slug});
});
}
},
posts: function () {

View file

@ -26,67 +26,33 @@ User = GhostBookshelf.Model.extend({
tableName: 'users',
hasTimestamps: true,
permittedAttributes: [
'id', 'uuid', 'full_name', 'password', 'email_address', 'profile_picture', 'cover_picture', 'bio', 'url', 'location',
'created_at', 'created_by', 'updated_at', 'updated_by'
'id', 'uuid', 'name', 'slug', 'password', 'email', 'image', 'cover', 'bio', 'website', 'location',
'accessibility', 'status', 'language', 'meta_title', 'meta_description', 'created_at', 'created_by',
'updated_at', 'updated_by'
],
defaults: function () {
return {
uuid: uuid.v4()
};
},
parse: function (attrs) {
// temporary alias of name for full_name (will get changed in the schema)
if (attrs.full_name && !attrs.name) {
attrs.name = attrs.full_name;
}
// temporary alias of website for url (will get changed in the schema)
if (attrs.url && !attrs.website) {
attrs.website = attrs.url;
}
// temporary alias of email for email_address (will get changed in the schema)
if (attrs.email_address && !attrs.email) {
attrs.email = attrs.email_address;
}
// temporary alias of image for profile_picture (will get changed in the schema)
if (attrs.profile_picture && !attrs.image) {
attrs.image = attrs.profile_picture;
}
// temporary alias of cover for cover_picture (will get changed in the schema)
if (attrs.cover_picture && !attrs.cover) {
attrs.cover = attrs.cover_picture;
}
return attrs;
},
initialize: function () {
this.on('saving', this.saving, this);
this.on('saving', this.validate, this);
},
validate: function () {
GhostBookshelf.validator.check(this.get('email_address'), "Please check your email address. It does not seem to be valid.").isEmail();
GhostBookshelf.validator.check(this.get('email'), "Please check your email address. It does not seem to be valid.").isEmail();
GhostBookshelf.validator.check(this.get('bio'), "Your bio is too long. Please keep it to 200 chars.").len(0, 200);
if (this.get('url') && this.get('url').length > 0) {
GhostBookshelf.validator.check(this.get('url'), "Your website is not a valid URL.").isUrl();
if (this.get('website') && this.get('website').length > 0) {
GhostBookshelf.validator.check(this.get('website'), "Your website is not a valid URL.").isUrl();
}
return true;
},
saving: function () {
// Deal with the related data here
creating: function () {
var self = this;
// Remove any properties which don't belong on the post model
this.attributes = this.pick(this.permittedAttributes);
GhostBookshelf.Model.prototype.creating.call(this);
if (!this.get('slug')) {
// Generating a slug requires a db call to look for conflicting slugs
return this.generateSlug(User, this.get('name'))
.then(function (slug) {
self.set({slug: slug});
});
}
},
posts: function () {
@ -152,7 +118,7 @@ User = GhostBookshelf.Model.extend({
* @author javorszky
*/
// return this.forge({email_address: userData.email_address}).fetch().then(function (user) {
// return this.forge({email: userData.email}).fetch().then(function (user) {
// if (user !== null) {
// return when.reject(new Error('A user with that email address already exists.'));
// }
@ -168,7 +134,7 @@ User = GhostBookshelf.Model.extend({
// Finds the user by email, and checks the password
check: function (_userdata) {
return this.forge({
email_address: _userdata.email
email: _userdata.email
}).fetch({require: true}).then(function (user) {
return nodefn.call(bcrypt.compare, _userdata.pw, user.get('password')).then(function (matched) {
if (!matched) {
@ -220,7 +186,7 @@ User = GhostBookshelf.Model.extend({
var newPassword = Math.random().toString(36).slice(2, 12), // This is magick
user = null;
return this.forge({email_address: email}).fetch({require: true}).then(function (_user) {
return this.forge({email: email}).fetch({require: true}).then(function (_user) {
user = _user;
return nodefn.call(bcrypt.hash, newPassword, null, null);
}).then(function (hash) {