Merge branch 'api-fun'

This commit is contained in:
Hannah Wolfe 2013-05-16 22:21:41 +01:00
commit 52242a5d10
18 changed files with 462 additions and 414 deletions

27
app.js
View File

@ -1,19 +1,22 @@
// # Ghost main app file // # Ghost main app file
/*global require */ /*global require, __dirname */
(function () { (function () {
"use strict"; "use strict";
// Module dependencies. // Module dependencies.
var express = require('express'), var express = require('express'),
fs = require('fs'),
admin = require('./core/admin/controllers'), admin = require('./core/admin/controllers'),
frontend = require('./core/frontend/controllers'), frontend = require('./core/frontend/controllers'),
api = require('./core/shared/api'),
flash = require('connect-flash'), flash = require('connect-flash'),
Ghost = require('./core/ghost'), Ghost = require('./core/ghost'),
I18n = require('./core/lang/i18n'), I18n = require('./core/lang/i18n'),
helpers = require('./core/frontend/helpers'), helpers = require('./core/frontend/helpers');
auth,
var auth,
// ## Variables // ## Variables
/** /**
@ -46,11 +49,13 @@
/** /**
* API routes.. * API routes..
* @todo convert these into a RESTful, public, authenticated API! * @todo auth should be public auth not user auth
*/ */
ghost.app().post('/api/v0.1/posts/create', auth, admin.posts.create); ghost.app().get('/api/v0.1/posts', auth, api.requestHandler(api.posts.browse));
ghost.app().post('/api/v0.1/posts/edit', auth, admin.posts.edit); ghost.app().get('/api/v0.1/posts/:id', auth, api.requestHandler(api.posts.read));
ghost.app().get('/api/v0.1/posts', auth, admin.posts.index); ghost.app().post('/api/v0.1/posts/create', auth, api.requestHandler(api.posts.add));
ghost.app().put('/api/v0.1/posts/edit', auth, api.requestHandler(api.posts.edit));
ghost.app()['delete']('/api/v0.1/posts/:id', auth, api.requestHandler(api.posts.destroy));
/** /**
* Admin routes.. * Admin routes..
@ -75,6 +80,10 @@
ghost.app().listen(3333, function () { ghost.app().listen(3333, function () {
console.log("Express server listening on port " + 3333); console.log("Express server listening on port " + 3333);
console.log('process: ', process.env);
}); });
// }, function (e) {
// console.log(e.toString());
// }).then(null, function (e) {
// console.log(e.stack);
// });
}()); }());

View File

@ -55,6 +55,19 @@
*/ */
config.homepage.posts = 4; config.homepage.posts = 4;
config.database = {
development: {
client: 'sqlite3',
connection: {
filename: './core/shared/data/testdb.db'
}
},
staging: {},
production: {}
};
/** /**
* @property {Object} exports * @property {Object} exports
*/ */

View File

@ -56,7 +56,7 @@
function save() { function save() {
var entry = { var entry = {
title: document.getElementById('entry-title').value, title: document.getElementById('entry-title').value,
markdown: editor.getValue() content: editor.getValue()
}, },
urlSegments = window.location.pathname.split('/'); urlSegments = window.location.pathname.split('/');
@ -64,7 +64,7 @@
entry.id = urlSegments[3]; entry.id = urlSegments[3];
$.ajax({ $.ajax({
url: '/api/v0.1/posts/edit', url: '/api/v0.1/posts/edit',
method: 'POST', method: 'PUT',
data: entry, data: entry,
success: function (data) { success: function (data) {
console.log('response', data); console.log('response', data);

View File

@ -1,11 +1,12 @@
/*global require, module */
(function () { (function () {
"use strict"; "use strict";
var Ghost = require('../../ghost'), var Ghost = require('../../ghost'),
_ = require('underscore'), _ = require('underscore'),
fs = require('fs'), fs = require('fs'),
Showdown = require('showdown'), when = require('when/node/function'),
converter = new Showdown.converter(), api = require('../../shared/api'),
ghost = new Ghost(), ghost = new Ghost(),
adminNavbar, adminNavbar,
@ -60,12 +61,13 @@
}, },
'editor': function (req, res) { 'editor': function (req, res) {
if (req.params.id !== undefined) { if (req.params.id !== undefined) {
ghost.dataProvider().posts.findOne({'id': parseInt(req.params.id, 10)}, function (error, post) { api.posts.read({id: parseInt(req.params.id, 10)})
.then(function (post) {
res.render('editor', { res.render('editor', {
bodyClass: 'editor', bodyClass: 'editor',
adminNav: setSelected(adminNavbar, 'blog'), adminNav: setSelected(adminNavbar, 'blog'),
title: post.title, title: post.get('title'),
content: post.content content: post.get('content')
}); });
}); });
} else { } else {
@ -76,11 +78,12 @@
} }
}, },
'blog': function (req, res) { 'blog': function (req, res) {
ghost.dataProvider().posts.findAll(function (error, posts) { api.posts.browse()
.then(function (posts) {
res.render('blog', { res.render('blog', {
bodyClass: 'manage', bodyClass: 'manage',
adminNav: setSelected(adminNavbar, 'blog'), adminNav: setSelected(adminNavbar, 'blog'),
posts: posts posts: posts.toJSON()
}); });
}); });
}, },
@ -119,47 +122,6 @@
res.redirect('/ghost/debug'); res.redirect('/ghost/debug');
}); });
} }
},
'posts': {
'index': function (req, res) {
},
'create': function (req, res) {
var entry = {
title: req.body.title,
content: req.body.markdown,
contentHtml: '',
language: ghost.config().defaultLang,
status: ghost.statuses().draft,
featured: false
};
entry.contentHtml = converter.makeHtml(entry.content);
ghost.dataProvider().posts.add(entry, function (error, post) {
if (!error) {
console.log('added', post);
res.json({id: post.id});
} else {
res.json(400, {error: post.errors});
}
});
},
'edit': function (req, res) {
var entry = {
id: parseInt(req.body.id, 10),
title: req.body.title,
content: req.body.markdown,
contentHtml: ''
};
entry.contentHtml = converter.makeHtml(entry.content);
ghost.dataProvider().posts.edit(entry, function (error, post) {
console.log('edited', post);
res.json({id: parseInt(post.id, 10)});
});
}
} }
}; };

View File

@ -18,7 +18,7 @@
<ol> <ol>
{{#each posts}} {{#each posts}}
{{! #if featured class="featured"{{/if}} {{! #if featured class="featured"{{/if}}
<li data-id="{{id}}" data-content="{{contentHtml}}"> <li data-id="{{id}}" data-content="{{content_html}}">
<a class="permalink" href="#"> <a class="permalink" href="#">
<h3 class="entry-title">{{title}}</h3> <h3 class="entry-title">{{title}}</h3>
<section class="entry-meta"> <section class="entry-meta">

View File

@ -7,45 +7,27 @@
'use strict'; 'use strict';
var Ghost = require('../../ghost'), var Ghost = require('../../ghost'),
_ = require('underscore'), api = require('../../shared/api'),
ghost = new Ghost(), ghost = new Ghost(),
frontendControllers; frontendControllers;
frontendControllers = { frontendControllers = {
'homepage': function (req, res) { 'homepage': function (req, res) {
var featureCount = 0, api.posts.browse().then(function (posts) {
postCount = 0, ghost.doFilter('prePostsRender', posts.toJSON(), function (posts) {
data; res.render('index', {posts: posts, ghostGlobals: ghost.globals()});
ghost.dataProvider().posts.findAll(function (error, posts) {
data = _.groupBy(posts, function (post) {
var group = null;
if (post.featured === true && featureCount < ghost.config().homepage.features) {
featureCount += 1;
group = 'features';
} else if (postCount < ghost.config().homepage.posts) {
postCount += 1;
group = 'posts';
}
return group;
});
ghost.doFilter('prepostsRender', data.posts, function (posts) {
res.render('index', {features: data.features, posts: posts, ghostGlobals: ghost.globals()});
}); });
}); });
}, },
'single': function (req, res) { 'single': function (req, res) {
ghost.dataProvider().posts.findOne({'slug': req.params.slug}, function (error, post) { api.posts.read({'slug': req.params.slug}).then(function (post) {
ghost.doFilter('prePostsRender', post, function (post) { ghost.doFilter('prePostsRender', post.toJSON(), function (post) {
res.render('single', {post: post, ghostGlobals: ghost.globals()}); res.render('single', {post: post, ghostGlobals: ghost.globals()});
}); });
}); });
} }
}; };
module.exports = frontendControllers; module.exports = frontendControllers;
}()); }());

View File

@ -1,11 +1,7 @@
// # Ghost Module // # Ghost Module
// Defines core methods required to build the frontend // Defines core methods required to build the frontend
/** /*global module, require, __dirname */
* global module,
* require,
* __dirname
**/
(function () { (function () {
"use strict"; "use strict";
@ -16,10 +12,13 @@
hbs = require('express-hbs'), hbs = require('express-hbs'),
_ = require('underscore'), _ = require('underscore'),
Polyglot = require('node-polyglot'), Polyglot = require('node-polyglot'),
JsonDataProvider = require('./shared/models/dataProvider.json'), JsonDataProvider = require('./shared/models/dataProvider.json'),
jsonDataProvider = new JsonDataProvider(), jsonDataProvider = new JsonDataProvider(),
JugglingDataProvider = require('./shared/models/dataProvider.juggling'), // JugglingDataProvider = require('./shared/models/dataProvider.juggling'),
jugglingDataProvider = new JugglingDataProvider(), // jugglingDataProvider = new JugglingDataProvider(),
BookshelfDataProvider = require('./shared/models/dataProvider.bookshelf'),
bookshelfDataProvider = new BookshelfDataProvider(),
Ghost, Ghost,
instance, instance,
filterCallbacks = {}, filterCallbacks = {},
@ -70,7 +69,7 @@
app: function () { return app; }, app: function () { return app; },
config: function () { return config; }, config: function () { return config; },
globals: function () { return globals; }, // there's no management here to be sure this has loaded globals: function () { return globals; }, // there's no management here to be sure this has loaded
dataProvider: function () { return jugglingDataProvider; }, dataProvider: function () { return bookshelfDataProvider; },
statuses: function () { return statuses; }, statuses: function () { return statuses; },
polyglot: function () { return polyglot; }, polyglot: function () { return polyglot; },
paths: function () { paths: function () {

75
core/shared/api.js Normal file
View File

@ -0,0 +1,75 @@
// # Ghost Data API
// Provides access to the data model
/**
* This is intended to replace the old dataProvider files and should access & manipulate the models directly
*/
/*global module, require */
(function () {
"use strict";
var Ghost = require('../ghost'),
when = require('when/node/function'),
_ = require('underscore'),
ghost = new Ghost(),
posts,
users,
requestHandler;
// # Posts
posts = {
// takes filter / pagination parameters
// returns a list of posts in a json response
browse: function (options) {
return when.call(ghost.dataProvider().posts.findAll, options);
},
// takes an identifier (id or slug?)
// returns a single post in a json response
read: function (args) {
return when.call(ghost.dataProvider().posts.findOne, args);
},
// takes a json object with all the properties which should be updated
// returns the resulting post in a json response
edit: function (postData) {
return when.call(ghost.dataProvider().posts.edit, postData);
},
// takes a json object representing a post,
// returns the resulting post in a json response
add: function (postData) {
return when.call(ghost.dataProvider().posts.add, postData);
},
// takes an identifier (id or slug?)
// returns a json response with the id of the deleted post
destroy: function (id) {
return when.call(ghost.dataProvider().posts.destroy, id);
}
};
// # Users
users = {};
// settings: {},
// categories: {},
// post_categories: {}
// requestHandler
// decorator for api functions which are called via an HTTP request
// takes the API method and wraps it so that it gets data from the request and returns a sensible JSON response
requestHandler = function (apiMethod) {
return function (req, res) {
var options = _.extend(req.body, req.params);
return apiMethod(options).then(function (result) {
res.json(result);
}, function (error) {
res.json(400, {error: error});
});
};
};
module.exports.posts = posts;
module.exports.users = users;
module.exports.requestHandler = requestHandler;
}());

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,20 +0,0 @@
[
{
"key": "url",
"value": "http://localhost:3333",
"createdBy": 1,
"updatedBy": 1
},
{
"key": "title",
"value": "John O'Nolan",
"createdBy": 1,
"updatedBy": 1
},
{
"key": "description",
"value": "Interactive designer, public speaker, startup advisor and writer. Living in Austria, attempting world domination via keyboard.",
"createdBy": 1,
"updatedBy": 1
}
]

View File

@ -1,15 +0,0 @@
[
{
"id": "1",
"username": "johnonolan",
"firstName": "John",
"lastName": "O'Nolan",
"emailAddress": "john@onolan.org",
"profilePicture": "logo.png",
"coverPicture": "",
"bio": "Interactive designer, public speaker, startup advisor and writer. Living in Austria, attempting world domination via keyboard.",
"url": "john.onolan.org",
"createdBy": 1,
"updatedBy": 1
}
]

View File

@ -0,0 +1,75 @@
/*global require, exports */
(function () {
"use strict";
var when = require('when'),
knex = require('./knex_init'),
fixtures = require('../fixtures/001'),
up,
down;
up = function () {
return when.all([
knex.Schema.createTable('posts', function (t) {
t.increments().primary();
t.string('title');
t.string('slug');
t.text('content');
t.text('content_html');
t.bool('featured');
t.string('image');
t.string('status');
t.string('language');
t.date('created_at');
t.integer('created_by');
t.date('updated_at');
t.integer('updated_by');
}),
knex.Schema.createTable('users', function (t) {
t.increments().primary();
t.string('username');
t.string('first_name');
t.string('last_name');
t.string('email_address');
t.string('profile_picture');
t.string('cover_picture');
t.text('bio');
t.string('url');
t.date('created_at');
t.integer('created_by');
t.date('updated_at');
t.integer('updated_by');
}),
knex.Schema.createTable('settings', function (t) {
t.increments().primary();
t.string('key');
t.text('value');
t.date('created_at');
t.integer('created_by');
t.date('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('settings').insert(fixtures.settings)
]);
});
};
down = function () {};
exports.up = up;
exports.down = down;
}());

View File

@ -0,0 +1,86 @@
/**
* Provides access to data via the Bookshelf ORM
*/
/*globals module, require, process */
(function () {
"use strict";
var knex = require('knex'),
models = require('./models'),
DataProvider,
instance;
knex.Initialize(require('../../../config').database[process.env.NODE_ENV || 'development']);
DataProvider = function () {
if (!instance) {
instance = this;
knex.Schema.hasTable('posts').then(null, function () {
// Simple boostraping of the data model for now.
require('./../data/migration/001').up().then(function () {
console.log('all done....');
});
});
}
return instance;
};
DataProvider.prototype.posts = function () { };
/**
* Naive find all
* @param args
* @param callback
*/
DataProvider.prototype.posts.findAll = function (args, callback) {
models.Posts.forge().fetch().then(function (posts) {
callback(null, posts);
}, callback);
};
/**
* Naive find one where args match
* @param args
* @param callback
*/
DataProvider.prototype.posts.findOne = function (args, callback) {
models.Post.forge(args).fetch().then(function (post) {
callback(null, post);
}, callback);
};
/**
* Naive add
* @param _post
* @param callback
*/
DataProvider.prototype.posts.add = function (_post, callback) {
models.Post.forge(_post).save().then(function (post) {
callback(null, post);
}, callback);
};
/**
* Naive edit
* @param _post
* @param callback
*/
DataProvider.prototype.posts.edit = function (_post, callback) {
models.Post.forge({id: _post.id}).fetch().then(function (post) {
post.set(_post).save().then(function (post) {
callback(null, post);
}, callback);
});
};
DataProvider.prototype.posts.destroy = function (_identifier, callback) {
models.Post.forge({id: _identifier}).destroy().then(function () {
callback(null);
});
};
module.exports = DataProvider;
}());

View File

@ -1,139 +0,0 @@
/**
* Provides access to data via the JugglingDb ORM
*/
/*globals module, require */
(function () {
"use strict";
var schema = require('./schema').schema,
fs = require('fs'),
_ = require('underscore'),
DataProvider,
instance;
function populateData(callback) {
// TODO: convert to promises
schema.models.Setting.findOne({}, function (error, data) {
if (data === null) {
// we haven't loaded any data yet
fs.readFile(__dirname + '/data/fixtures/users.json', function (error, data) {
if (error) {
throw error;
}
var users = JSON.parse(data);
_.each(users, function (post) {
schema.models.User.create(post, function (error, data) {
console.log('User created error', error);
console.log('User created data', data);
});
});
fs.readFile(__dirname + '/data/fixtures/posts.json', function (error, data) {
if (error) {
throw error;
}
var posts = JSON.parse(data),
post;
_.each(posts, function (_post) {
post = new schema.models.Post(_post);
post.preCreate(function () {
post.save(function (error, data) {
console.log('Post created error', error);
console.log('Post created data', data);
});
});
});
fs.readFile(__dirname + '/data/fixtures/settings.json', function (error, data) {
if (error) {
throw error;
}
var posts = JSON.parse(data);
_.each(posts, function (post) {
schema.models.Setting.create(post, function (error, data) {
console.log('Setting created error', error);
console.log('Setting created data', data);
});
});
callback();
});
});
});
} else {
callback();
}
});
}
DataProvider = function () {
if (!instance) {
instance = this;
if (process.env.forcePopulate) {
populateData();
}
}
return instance;
};
DataProvider.prototype.posts = function () {};
/**
* Naive find all
* @param callback
*/
DataProvider.prototype.posts.findAll = function (callback) {
schema.models.Post.all(callback);
};
/**
* Naive find one where args match
* @param callback
*/
DataProvider.prototype.posts.findOne = function (args, callback) {
schema.models.Post.findOne({where: args}, callback);
};
/**
* Naive add
* @param post
* @param callback
*/
DataProvider.prototype.posts.add = function (_post, callback) {
var post = new schema.models.Post(_post);
post.preCreate(function () {
post.save(callback);
});
};
/**
* Naive edit
* @param post
* @param callback
*/
DataProvider.prototype.posts.edit = function (_post, callback) {
schema.models.Post.findOne({where: {id: _post.id}}, function (error, post) {
post = _.extend(post, _post);
schema.models.Post.updateOrCreate(post, callback);
});
};
DataProvider.prototype.populateData = populateData;
module.exports = DataProvider;
}());

View File

@ -0,0 +1,86 @@
/*global require, module */
(function () {
"use strict";
// We should just be able to require bookshelf and have it reference
// the `Knex` instance bootstraped at the app initialization.
var Bookshelf = require('bookshelf'),
Showdown = require('showdown'),
converter = new Showdown.converter(),
Post,
Posts,
User,
Setting;
Post = Bookshelf.Model.extend({
tableName: 'posts',
hasTimestamps: true,
initialize: function () {
this.on('creating', this.creating, this);
this.on('saving', this.saving, this);
},
saving: function () {
if (!this.get('title')) {
throw new Error('Post title cannot be blank');
}
this.set('content_html', converter.makeHtml(this.get('content')));
// refactoring of ghost required in order to make these details available here
// this.set('language', this.get('language') || ghost.config().defaultLang);
// this.set('status', this.get('status') || ghost.statuses().draft);
},
creating: function () {
if (!this.get('slug')) {
this.generateSlug();
}
},
generateSlug: function () {
return this.set('slug', this.get('title').replace(/\:/g, '').replace(/\s/g, '-').toLowerCase());
},
user: function () {
return this.belongsTo(User, 'created_by');
}
});
Posts = Bookshelf.Collection.extend({
model: Post
});
User = Bookshelf.Model.extend({
tableName: 'users',
hasTimestamps: true,
posts: function () {
return this.hasMany(Posts, 'created_by');
}
});
Setting = Bookshelf.Model.extend({
tableName: 'settings'
});
module.exports = {
Post: Post,
Posts: Posts,
User: User,
Setting: Setting
};
}());

View File

@ -1,111 +0,0 @@
/**
* Database Schema, created with JugglingDB
*
* Vastly incomplete!
*/
/*globals module, require */
(function () {
"use strict";
var Schema = require('jugglingdb').Schema,
schema = new Schema('sqlite3', {
database: __dirname + '/../data/datastore.db'
}),
Post,
User,
Setting;
/*------------------------------------------------------------------------------------
POST / Post / Posts
------------------------------------------------------------------------------------*/
Post = schema.define('Post', {
title: String,
slug: String,
content: Schema.Text,
contentHtml: Schema.Text,
featured: Boolean,
image: String,
status: String,
language: String,
createdAt: Date,
createdBy: Number,
updatedAt: Date,
updatedBy: Number
});
Post.prototype.generateSlug = function () {
return this.title.replace(/\:/g, '').replace(/\s/g, '-').toLowerCase();
};
Post.prototype.preCreate = function (next) {
console.log('pre create 1', this);
if (this.createdAt === undefined) {
this.createdAt = new Date();
}
if (this.slug === undefined) {
this.slug = this.generateSlug();
}
console.log('pre create 2', this);
next();
};
// Validations
Post.validatesPresenceOf('title', {message: 'Post title cannot be blank'});
//Post.validatesPresenceOf('slug');
//Post.validatesPresenceOf('language', {message: 'Language cannot be blank'});
//Post.validatesUniquenessOf('slug');
//Post.validatesLengthOf('language', {min: 2, max: 5}, "The language code should be between 2 and 5 chars long, E.g. 'en' or 'en_GB' ");
Post.beforeSave = function (next, data) {
console.log('before s1', data);
// set updated
data.updatedAt = new Date();
next();
};
/*------------------------------------------------------------------------------------
USER / User / Users
------------------------------------------------------------------------------------*/
User = schema.define('User', {
username: String,
firstName: String,
lastName: String,
emailAddress: String,
profilePicture: String,
coverPicture: String,
bio: Schema.Text,
url: String,
createdAt: Date,
createdBy: Number,
updatedAt: Date,
updatedBy: Number
});
/*------------------------------------------------------------------------------------
SETTING / Setting / Settings
------------------------------------------------------------------------------------*/
Setting = schema.define('Setting', {
key: String,
value: Schema.Text,
createdAt: Date,
createdBy: Number,
updatedAt: Date,
updatedBy: Number
});
/*------------------------------------------------------------------------------------
RELATIONSHIPS
------------------------------------------------------------------------------------*/
User.hasMany(Post, {as: 'posts', foreignKey: 'createdBy'});
Post.belongsTo(User, {as: 'author', foreignKey: 'createdBy'});
schema.autoupdate();
exports.schema = schema;
}());

View File

@ -16,7 +16,10 @@
"underscore": "*", "underscore": "*",
"showdown": "*", "showdown": "*",
"sqlite3": "2.1.7", "sqlite3": "2.1.7",
"jugglingdb": "0.2.x", "bookshelf": "0.1.x",
"knex": "0.1.x",
"when": "~2.1.0",
"jugglingdb": "0.2.0-29",
"jugglingdb-sqlite3": "git+https://github.com/jugglingdb/sqlite3-adapter.git#master" "jugglingdb-sqlite3": "git+https://github.com/jugglingdb/sqlite3-adapter.git#master"
}, },
"devDependencies": { "devDependencies": {