summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/Readme.md4
-rw-r--r--app/index.js141
-rw-r--r--app/node_modules/okdb/index.js73
-rw-r--r--app/node_modules/okdb/package.json16
-rw-r--r--app/node_modules/okquery/index.js56
-rw-r--r--app/node_modules/okquery/package.json13
-rw-r--r--app/node_modules/okresource/index.js64
-rw-r--r--app/node_modules/okresource/package.json15
-rw-r--r--app/node_modules/okrest/index.js20
-rw-r--r--app/node_modules/okrest/package.json11
-rw-r--r--app/node_modules/okserver/index.js20
-rw-r--r--app/node_modules/okserver/package.json14
-rw-r--r--app/node_modules/oktemplate/index.js36
-rw-r--r--app/node_modules/oktemplate/package.json15
-rw-r--r--app/node_modules/okview/index.js132
-rw-r--r--app/node_modules/okview/package.json16
16 files changed, 646 insertions, 0 deletions
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"
+ }
+}