diff options
| -rw-r--r-- | .gitignore | 6 | ||||
| -rw-r--r-- | app/Readme.md | 4 | ||||
| -rw-r--r-- | app/index.js | 141 | ||||
| -rw-r--r-- | app/node_modules/okdb/index.js | 73 | ||||
| -rw-r--r-- | app/node_modules/okdb/package.json | 16 | ||||
| -rw-r--r-- | app/node_modules/okquery/index.js | 56 | ||||
| -rw-r--r-- | app/node_modules/okquery/package.json | 13 | ||||
| -rw-r--r-- | app/node_modules/okresource/index.js | 64 | ||||
| -rw-r--r-- | app/node_modules/okresource/package.json | 15 | ||||
| -rw-r--r-- | app/node_modules/okrest/index.js | 20 | ||||
| -rw-r--r-- | app/node_modules/okrest/package.json | 11 | ||||
| -rw-r--r-- | app/node_modules/okserver/index.js | 20 | ||||
| -rw-r--r-- | app/node_modules/okserver/package.json | 14 | ||||
| -rw-r--r-- | app/node_modules/oktemplate/index.js | 36 | ||||
| -rw-r--r-- | app/node_modules/oktemplate/package.json | 15 | ||||
| -rw-r--r-- | app/node_modules/okview/index.js | 132 | ||||
| -rw-r--r-- | app/node_modules/okview/package.json | 16 | ||||
| -rw-r--r-- | examples/index.js | 51 | ||||
| -rw-r--r-- | examples/www/index.mustache | 13 | ||||
| -rw-r--r-- | examples/www/page.mustache | 5 | ||||
| -rw-r--r-- | examples/www/project.mustache | 2 |
21 files changed, 722 insertions, 1 deletions
@@ -1,3 +1,7 @@ -node_modules/ +# Ignore top level and nested deps +/node_modules/ +/app/node_modules/**/node_modules/ +.bin/ *.swp .DS_Store +db.json diff --git a/app/Readme.md b/app/Readme.md new file mode 100644 index 0000000..ba77ed7 --- /dev/null +++ b/app/Readme.md @@ -0,0 +1,4 @@ +This folder contains the bulk of the server source. + +Source files are stored in a `node_modules/` rather than a `lib/` to +promote dividing components in a modular way. diff --git a/app/index.js b/app/index.js new file mode 100644 index 0000000..641a468 --- /dev/null +++ b/app/index.js @@ -0,0 +1,141 @@ +var path = require('path'); +var OKQuery = require('okquery'); +var OKView = require('okview'); +var OKDB = require('okdb'); +var OKResource = require('okresource') +var OKTemplate = require('oktemplate'); +var OKServer = require('okserver'); +var OKRestEndpoint = require('okrest'); + +/** + * OKCMS! + * Basically takes configuration and gives you a server. + */ +function OKCMS(options) { + options = options || {}; + var root = this._root = options.root || 'www'; + // Reduce resource config into unique set + this._resourceConfig = options.resources || []; + this._views = options.views || { + '/': { template: 'index' } + }; + this._resourceRoot = options.resourceRoot || '/api'; + this._server = new OKServer({root: root}); + this._templates = new OKTemplate({root: root}); + var db = this._db = options.db || new OKDB(); + // Special query for getting meta info + this._meta = options.meta || { + name: 'meta', + get: function() { + return db.getMeta(); + } + }; + this._init(); +} + +OKCMS.prototype.listen = function listen(port, options) { + options = options || {}; + this._server.listen(port); +}; + +OKCMS.prototype._init = function _init() { + var self = this; + + // Create resources instances from config and add CRUD views on them + // NOTE Does not support subresources + this._resources = this._resourceConfig.map(function (config) { + var resource = self._createResource(config); + var route = path.join(self._resourceRoot, resource.name); + self._server.addView(route, resource.view()); + return resource; + }); + + // Add HTML views + Object.keys(this._views) + // Make sure more specific routes are processed first + // TODO This is not semantically correct (bro) + .sort(function(a, b) { + return a.length < b.length; + }) + // For each route / viewOptions pair, add a view + .forEach(function eachRoute(route) { + var options = self._views[route]; + // View should know what its route is. + options.route = route; + var view = self._createView(options); + // Add view at route + self._server.addView(route, view); + }); +}; + +/** + * Takes a resource configuration object and returns a resource instance + */ +OKCMS.prototype._createResource = function(options) { + options = options || {}; + var resourceClass = options.resource; + // No resource? Bail! + if (!resourceClass) throw new Error('OKCMS: No resource class given'); + // Default to 'select all' id + options.id = options.id || '*'; + // Some dependency injection + options.db = this._db; + // Clean unpassed options + delete options.resource; + // Return resource instance + return resourceClass(options); +}; + +/** + * Takes a view configuration object and returns a view instance + */ +OKCMS.prototype._createView = function _createView(options) { + options = options || {}; + var self = this; + var viewClass = options.view || OKView; + var template = this._templates.getTemplate(options.template); + var queryConfig = options.data || []; + // No template? Bail! + if (!template) throw new Error('OKCMS: No template "' + options.template + '"'); + // Inject them dependencies + options.template = template; + options.meta = this._meta; + options.queries = this._createQueries(queryConfig); + // Clean options not passed to view + delete options.view; + // Return view instance + return viewClass(options); +}; + +/** + * Takes a query configuration and returns a list of queries + */ +OKCMS.prototype._createQueries = function(options) { + options = options || []; + var db = this._db; + if (!options.length) + options = [options]; + return options.map(function(option) { + if (!option.name) + throw new Error('No document name provided to query'); + option.id = option.id || '*'; + return new OKQuery(db, { + name: option.name, + id: option.id + }); + }); +}; + +module.exports = { + + createApp: function(options) { + return new OKCMS(options); + }, + + OKResource: OKResource, + + OKView: OKView, + + OKRestEndpoint: OKRestEndpoint + +}; diff --git a/app/node_modules/okdb/index.js b/app/node_modules/okdb/index.js new file mode 100644 index 0000000..f32627c --- /dev/null +++ b/app/node_modules/okdb/index.js @@ -0,0 +1,73 @@ +var Q = require('q'); +var format = require('util').format; +var low = require('lowdb'); +low.mixin(low.mixin(require('underscore-db'))); + +/** + * OKDB! + * Minimal interface over a database of named collections of documents. + */ +function OKDB(options) { + if (!(this instanceof OKDB)) return new OKDB(options); + options = options || {}; + this._db = options.db || JSONDown('db'); +} + +OKDB.prototype.getMeta = function() { + return this._db.getMeta(); +}; + +OKDB.prototype.putMeta = function(meta) { + return this._db.putMeta(meta); +} + +OKDB.prototype.get = function(collection, id) { + return this._db.get(collection, id); +}; + +OKDB.prototype.getAll = function(collection) { + return this._db.getAll(collection); +}; + +OKDB.prototype.put = function(collection, id) { + return this._db.put(collection, id); +}; + +OKDB.prototype.putBatch = function(collection, data) { + return this._db.putBatch(collection, data); +}; + +/** + * DB implementation backed by a JSON file. + * TODO Unfinished + */ +function JSONDown(name, options) { + if (!(this instanceof JSONDown)) return new JSONDown(name, options); + options = options || {}; + var filename = name + '.json'; + this._db = low(filename); +} + +JSONDown.prototype._resolve = function(data) { + return Q.Promise(function resolvePromise(resolve, reject) { + resolve(data); + }); +}; + +JSONDown.prototype.get = function(collection, id) { + var data = this._db(collection).get(id); + return this._resolve(data || {}); +}; + +JSONDown.prototype.getMeta = function() { + var data = this._db('meta').first(); + return this._resolve(data || {}); +}; + +JSONDown.prototype.getAll = function(collection) { + var data = this._db(collection).toArray(); + return this._resolve(data || []); +}; + +module.exports = OKDB; +module.exports.JSONDown = JSONDown; diff --git a/app/node_modules/okdb/package.json b/app/node_modules/okdb/package.json new file mode 100644 index 0000000..659422e --- /dev/null +++ b/app/node_modules/okdb/package.json @@ -0,0 +1,16 @@ +{ + "name": "okdb", + "version": "1.0.0", + "description": "nice", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "OKFocus", + "license": "None", + "dependencies": { + "lowdb": "^0.7.2", + "q": "^1.2.0", + "underscore-db": "^0.8.1" + } +} diff --git a/app/node_modules/okquery/index.js b/app/node_modules/okquery/index.js new file mode 100644 index 0000000..22dd74f --- /dev/null +++ b/app/node_modules/okquery/index.js @@ -0,0 +1,56 @@ +/** + * OKQuery! + * Takes configuration and gives you something that can run a DB query + * based on the configurations. + */ +function OKQuery(db, options) { + if (!(this instanceof OKQuery)) return new OKQuery(db, options); + options = options || {}; + if (!db) + throw new Error('No DB provided to query.'); + if (!options.name) + throw new Error('No name type provided to query'); + this.name = options.name; + this.get = createQuery(db, options); +} + +function createQuery(db, config) { + var name = config.name; + var id = config.id || '*'; + if (isDynamic(id)) { + return queryDynamic(db, name); + } else if (isSet(id)) { + return queryAll(db, name); + } else { + return querySingle(db, name, id); + } +} + +function queryDynamic(db, name) { + return function(options) { + options = options || {}; + return db.get(name, options.id); + } +} + +function queryAll(db, name) { + return function() { + return db.getAll(name); + } +} + +function querySingle(db, name, id) { + return function() { + return db.get(name, id); + } +} + +function isDynamic(id) { + return id && id.charAt(0) === ':'; +} + +function isSet(id) { + return id && id === '*'; +} + +module.exports = OKQuery; diff --git a/app/node_modules/okquery/package.json b/app/node_modules/okquery/package.json new file mode 100644 index 0000000..a503c4b --- /dev/null +++ b/app/node_modules/okquery/package.json @@ -0,0 +1,13 @@ +{ + "name": "okquery", + "version": "1.0.0", + "description": "good", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "OKFocus", + "license": "None", + "dependencies": { + } +} diff --git a/app/node_modules/okresource/index.js b/app/node_modules/okresource/index.js new file mode 100644 index 0000000..c46e7f4 --- /dev/null +++ b/app/node_modules/okresource/index.js @@ -0,0 +1,64 @@ +var Q = require('q'); +var joi = require('joi'); +var OKRestEndpoint = require('okrest'); + +/** + * Creates an OKResource types + * Takes a resource name and a spec defining the resources attributes. + */ +function createResourceClass(name, spec, options) { + options = options || {}; + spec = spec || {}; + // All resources have the same default CRUD endpoint + var viewClass = options.endpoint || OKRestEndpoint; + // Id determines specific resource referenced. + // Defaults to set of all resources of this type. + var id = options.id || '*'; + // The meta resource is a special case + var meta = options.meta === undefined ? false : options.meta; + if (viewClass !== OKRestEndpoint && !(viewClass instanceof OKRestEndpoint)) + throw new Error('Resource view not descendent of OKRestEndpoint'); + + /** + * OKResource! + */ + function OKResource(options) { + if (!(this instanceof OKResource)) return new OKResource(options); + options = options || {}; + this.name = name; + if (!name) + throw new Error('No resource type provided to resource!') + var db = this._db = options.db; + if (!db) + throw new Error('No DB provided to resource!'); + this._validator = compileValidator(spec); + } + + /** + * Returns the resource's CRUD view + * This allows us to specify custom views per resource if need be + */ + OKResource.prototype.view = function(options) { + return viewClass(this, options); + }; + + // OKResource.prototype.get = function() { + // return this._query(); + // }; + + // Expose the resource type on the class constructor + OKResource.type = name; + + return OKResource; +} + +/** + * Compiles our schema spec into a schema validator function + */ +function compileValidator(spec) { + // Skip validation for now + var schema = joi.any(); + return schema.validate.bind(schema); +} + +module.exports = createResourceClass; diff --git a/app/node_modules/okresource/package.json b/app/node_modules/okresource/package.json new file mode 100644 index 0000000..12fcd26 --- /dev/null +++ b/app/node_modules/okresource/package.json @@ -0,0 +1,15 @@ +{ + "name": "okresource", + "version": "1.0.0", + "description": "really good", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "OKFocus", + "license": "None", + "dependencies": { + "joi": "^6.0.8", + "q": "^1.2.0" + } +} diff --git a/app/node_modules/okrest/index.js b/app/node_modules/okrest/index.js new file mode 100644 index 0000000..169626d --- /dev/null +++ b/app/node_modules/okrest/index.js @@ -0,0 +1,20 @@ +var OKView = require('okview'); + +/** + * OKRestEndpoint! + * Takes a resources and creates a CRUD endpoint. + */ +function OKRestEndpoint(resource, options) { + if (!(this instanceof OKRestEndpoint)) return new OKRestEndpoint(resource, options); + options = options || {}; + this._resource = resource; +} + +OKRestEndpoint.prototype.middleware = function() { + var self = this; + return function handleREST(req, res, next) { + res.send(self._resource.name); + }; +} + +module.exports = OKRestEndpoint; diff --git a/app/node_modules/okrest/package.json b/app/node_modules/okrest/package.json new file mode 100644 index 0000000..462c890 --- /dev/null +++ b/app/node_modules/okrest/package.json @@ -0,0 +1,11 @@ +{ + "name": "okrest", + "version": "1.0.0", + "description": "nice", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "OKFocus", + "license": "None" +} diff --git a/app/node_modules/okserver/index.js b/app/node_modules/okserver/index.js new file mode 100644 index 0000000..4a513e9 --- /dev/null +++ b/app/node_modules/okserver/index.js @@ -0,0 +1,20 @@ +var express = require('express'); + +function OKServer(options) { + if (!(this instanceof OKServer)) return new OKServer(options); + options = options || {}; + this._app = express(); +} + +OKServer.prototype.addView = function addView(route, view) { + console.log(route, view) + this._app.use(route, view.middleware()); + return this; +}; + +OKServer.prototype.listen = function listen(port) { + this._app.listen(port || 1337); + return this; +}; + +module.exports = OKServer; diff --git a/app/node_modules/okserver/package.json b/app/node_modules/okserver/package.json new file mode 100644 index 0000000..d0b611a --- /dev/null +++ b/app/node_modules/okserver/package.json @@ -0,0 +1,14 @@ +{ + "name": "okserver", + "version": "1.0.0", + "description": "sweet", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "OKFocus", + "license": "None", + "dependencies": { + "express": "^4.12.3" + } +} diff --git a/app/node_modules/oktemplate/index.js b/app/node_modules/oktemplate/index.js new file mode 100644 index 0000000..4aec98b --- /dev/null +++ b/app/node_modules/oktemplate/index.js @@ -0,0 +1,36 @@ +var fs = require('fs'); +var path = require('path'); +var glob = require('glob'); +var hogan = require('hogan.js'); + +/** + * Manages templates. Only supports Mustache currently/ + */ +function OKTemplateRepo(options) { + options = options || {}; + this._root = options.root || 'www'; + this._ext = options.ext || '.mustache'; + this._cache = {}; + this._populateCache(this._cache); +} + +OKTemplateRepo.prototype.getTemplate = function getTemplate(name) { + return this._cache[name]; +} + +/** + * Go through our template dir and read the template files + * into memory as strings. + * Assumes all templates fit into memory. + */ +OKTemplateRepo.prototype._populateCache = function _populateCache(cache) { + var self = this; + var files = glob.sync(this._root + '/*' + this._ext); + files.forEach(function eachFile(file) { + var name = path.basename(file, self._ext); + var templateString = fs.readFileSync(file, {encoding: 'UTF8'}); + cache[name] = hogan.compile(templateString); + }); +} + +module.exports = OKTemplateRepo; diff --git a/app/node_modules/oktemplate/package.json b/app/node_modules/oktemplate/package.json new file mode 100644 index 0000000..c32e098 --- /dev/null +++ b/app/node_modules/oktemplate/package.json @@ -0,0 +1,15 @@ +{ + "name": "oktemplate", + "version": "1.0.0", + "description": "nice", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "OKFocus", + "license": "None", + "dependencies": { + "glob": "^5.0.3", + "hogan.js": "^3.0.2" + } +} diff --git a/app/node_modules/okview/index.js b/app/node_modules/okview/index.js new file mode 100644 index 0000000..341ebdd --- /dev/null +++ b/app/node_modules/okview/index.js @@ -0,0 +1,132 @@ +var Q = require('q'); +var isarray = require('lodash.isarray'); +var pluralize = require('pluralize'); +var OKResource = require('okresource'); + +// Routes for views over collections have a special pattern +// containing a free variable e.g. :id +var UNBOUND_ROUTE_PATTERN = /:([a-zA-Z\-_]+)/; + +/** + * OKView! + * Is supplied DB queries and a template and is responsible + * for resolving the queries, throwing the data into the templates, + * and sending the response. + */ +function OKView(options) { + if (!(this instanceof OKView)) return new OKView(options); + options = options || {}; + if (!options.template) throw new Error('No template provided to view.'); + if (!options.meta) throw new Error('No meta resource provided to view'); + if (!options.route) throw new Error('No route provided to view'); + this.route = options.route; + this._template = options.template; + this._meta = options.meta; + this._queries = options.queries || []; + // Whether this is a view for a specific resource or its + // resource will be resolved later + this.unbound = !!UNBOUND_ROUTE_PATTERN.exec(this.route); + this._middleware = createMiddleware(this); +} + +OKView.prototype.middleware = function() { + return this._middleware; +}; + +OKView.prototype.render = function(req, res, data) { + return res.send(this._template.render(data)); +}; + +/** + * Takes queries backing this view and transforms them + * into a promise for an object with all the queried data, + * suitable to pass to the template. + * + * Lil bit convoluted, sorry. + */ +OKView.prototype.getTemplateData = function(options) { + var self = this; + var queries = this._queries; + return Q.promise(function(resolve, reject) { + return Q.all( + [self._meta.get()].concat(queries.map(function(query) { + return query.get(options); + }))) + .then(function(results) { + var meta = results.shift(); + var normalized = results.reduce(function(data, result, i) { + var name = queries[i].name; + if (isarray(result)) { + data[pluralize(name)] = result; + } else { + data[name] = result; + } + return data; + }, {meta: meta}); + resolve(normalized); + }, reject); + }); +}; + +/** + * Unbound views need different middleware to resolve requests + */ +function createMiddleware(view) { + if (view.unbound) { + return collectionMiddleware(view); + } else { + return singleMiddleware(view); + } +} + +// Note that these middleware do not call next +// and should thus always be added at the end of the +// middleware chain. + +/** + * Creates middleware for a view which does not + * yet have a resource id associated with it + */ +function collectionMiddleware(view) { + var paramName = getParamName(view.route); + return function(req, res, next) { + view.getTemplateData({ + id: req.params[paramName] + }).then(function(data) { + view.render(req, res, data); + }, errorHandler(req, res, next)); + }; +} + +/** + * Creates middleware for a view which already + * has a resource id associated with it + */ +function singleMiddleware(view) { + return function(req, res, next) { + view.getTemplateData().then(function(data) { + view.render(req, res, data); + }, errorHandler(req, res, next)); + }; +} + +/** + * TODO BS error handling for now + */ +function errorHandler(req, res, next) { + return function(err) { + res.send(err.stack); + } +} + +/** + * Given a route with a free variable, return the + * name of the variable, e.g. :id returns id + */ +function getParamName(route) { + route = route || ''; + var matches = UNBOUND_ROUTE_PATTERN.exec(route) || []; + return matches[1]; +} + +module.exports = OKView; diff --git a/app/node_modules/okview/package.json b/app/node_modules/okview/package.json new file mode 100644 index 0000000..1204823 --- /dev/null +++ b/app/node_modules/okview/package.json @@ -0,0 +1,16 @@ +{ + "name": "okview", + "version": "1.0.0", + "description": "really good", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "OKFocus", + "license": "None", + "dependencies": { + "lodash.isarray": "^3.0.1", + "pluralize": "^1.1.2", + "q": "^1.2.0" + } +} diff --git a/examples/index.js b/examples/index.js new file mode 100644 index 0000000..1a7bbfb --- /dev/null +++ b/examples/index.js @@ -0,0 +1,51 @@ +var okcms = require('..'); + +var Page = okcms.OKResource('page', { + title: { name: 'string' }, + body: { name: 'string' } +}); + +var Project = okcms.OKResource('project', { + title: {name: 'string'}, + index: {name: 'integer'}, + category: {name: 'string'}, + body: {name: 'string'}, + videos: {name: 'array:uri'}, + images: {name: 'array:image'} +}); + +var app = okcms.createApp({ + + root: 'www', + + resources: [ + { resource: Page, id: 'about' }, + { resource: Page, id: 'contact' }, + { resource: Project, id: '*' }, + ], + + views: { + '/': { + template: 'index', + data: [ + {name: 'project', id: '*'}, + {name: 'page', id: '*'} + ] + }, + '/about': { + template: 'page', + data: {name: 'page', id: 'about'} + }, + '/contact': { + template: 'page', + data: {name: 'page', id: 'contact'} + }, + '/:id': { + template: 'project', + data: {name: 'project', id: ':id'} + } + } + +}).listen(1337); + +console.log('Server listening at port 1337...'); diff --git a/examples/www/index.mustache b/examples/www/index.mustache new file mode 100644 index 0000000..04d0632 --- /dev/null +++ b/examples/www/index.mustache @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + <head> + </head> + <body> + {{meta.project}} + <ul> + {{#projects}} + <li>{{id}}</li> + {{/projects}} + </ul> + </body> +</html> diff --git a/examples/www/page.mustache b/examples/www/page.mustache new file mode 100644 index 0000000..f7d899f --- /dev/null +++ b/examples/www/page.mustache @@ -0,0 +1,5 @@ +woop +<h1>{{page.title}}</h1> +<p> + {{page.body}} +</p> diff --git a/examples/www/project.mustache b/examples/www/project.mustache new file mode 100644 index 0000000..dac1d8d --- /dev/null +++ b/examples/www/project.mustache @@ -0,0 +1,2 @@ +babaganuj +{{project.id}} |
