From d587a845d45bb05fff34552dfb2181e66df013b8 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Sat, 14 Sep 2013 20:01:46 +0100 Subject: [PATCH] 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 --- core/server/data/migration/001.js | 128 ---------------------------- core/server/data/migration/002.js | 83 ------------------ core/server/data/migration/003.js | 49 ----------- core/server/data/migration/index.js | 4 +- core/server/models/base.js | 83 ++++++++++++++++++ core/server/models/permission.js | 15 +--- core/server/models/post.js | 106 +++++------------------ core/server/models/role.js | 15 +--- core/server/models/settings.js | 13 --- core/server/models/tag.js | 28 ++++-- core/server/models/user.js | 74 +++++----------- 11 files changed, 154 insertions(+), 444 deletions(-) delete mode 100644 core/server/data/migration/001.js delete mode 100644 core/server/data/migration/002.js delete mode 100644 core/server/data/migration/003.js diff --git a/core/server/data/migration/001.js b/core/server/data/migration/001.js deleted file mode 100644 index 9823bbebb1..0000000000 --- a/core/server/data/migration/001.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/core/server/data/migration/002.js b/core/server/data/migration/002.js deleted file mode 100644 index f298cba47f..0000000000 --- a/core/server/data/migration/002.js +++ /dev/null @@ -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; diff --git a/core/server/data/migration/003.js b/core/server/data/migration/003.js deleted file mode 100644 index a603689cf8..0000000000 --- a/core/server/data/migration/003.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/core/server/data/migration/index.js b/core/server/data/migration/index.js index 8db3e8ca98..24b6ceca8c 100644 --- a/core/server/data/migration/index.js +++ b/core/server/data/migration/index.js @@ -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 () { diff --git a/core/server/models/base.js b/core/server/models/base.js index 00b99be178..ad073be4f4 100644 --- a/core/server/models/base.js +++ b/core/server/models/base.js @@ -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); } }, { diff --git a/core/server/models/permission.js b/core/server/models/permission.js index befe29125f..8fa0688356 100644 --- a/core/server/models/permission.js +++ b/core/server/models/permission.js @@ -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); }, diff --git a/core/server/models/post.js b/core/server/models/post.js index e86e7258c5..73f8212f04 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -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) { diff --git a/core/server/models/role.js b/core/server/models/role.js index 76fa86199c..2e960dc73f 100644 --- a/core/server/models/role.js +++ b/core/server/models/role.js @@ -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); }, diff --git a/core/server/models/settings.js b/core/server/models/settings.js index 2472def6a7..46fc002ffd 100644 --- a/core/server/models/settings.js +++ b/core/server/models/settings.js @@ -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) { diff --git a/core/server/models/tag.js b/core/server/models/tag.js index 64581ff454..4f20e3af5f 100644 --- a/core/server/models/tag.js +++ b/core/server/models/tag.js @@ -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 () { diff --git a/core/server/models/user.js b/core/server/models/user.js index 27cdd1a26e..d30774ee57 100644 --- a/core/server/models/user.js +++ b/core/server/models/user.js @@ -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) {