summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/index.js9
-rw-r--r--app/node_modules/okadminview/index.js121
-rw-r--r--app/node_modules/okadminview/package.json3
-rw-r--r--app/node_modules/okdb/index.js214
-rw-r--r--app/node_modules/okdb/package.json3
-rw-r--r--app/node_modules/okquery/index.js12
-rw-r--r--app/node_modules/okquery/package.json1
-rw-r--r--app/node_modules/okresource/index.js125
-rw-r--r--app/node_modules/okschema/index.js42
-rw-r--r--app/node_modules/okserver/index.js10
-rw-r--r--app/node_modules/oktemplate/index.js15
-rw-r--r--app/node_modules/okview/index.js4
-rw-r--r--examples/db.json63
-rw-r--r--site/db.json14
-rw-r--r--themes/okadmin/public/css/main.css69
-rw-r--r--themes/okadmin/public/js/app.js45
-rw-r--r--themes/okadmin/templates/index.liquid37
-rw-r--r--themes/okadmin/templates/partials/flash.liquid (renamed from themes/okadmin/templates/partials/errors.liquid)9
-rw-r--r--themes/okadmin/templates/partials/inputs.liquid24
-rw-r--r--themes/okadmin/templates/resource.liquid4
-rw-r--r--themes/okadmin/templates/resource_new.liquid4
21 files changed, 597 insertions, 231 deletions
diff --git a/app/index.js b/app/index.js
index b312eb1..e462b48 100644
--- a/app/index.js
+++ b/app/index.js
@@ -77,8 +77,11 @@ function OKCMS(options) {
var adminTemplateProvider = this._adminTemplateProvider =
new OKTemplate({root: adminTemplateRoot});
- var db = new OKDB(options.db || 'fs');
var schemas = this._schemas = this._createSchemas(schemaConfig);
+ var db = new OKDB({
+ db: options.db || 'fs',
+ schemas: schemas
+ });
var resourceCache = this._resourceCache =
this._createResources(resourceConfig, db, schemas);
@@ -119,6 +122,8 @@ OKCMS.prototype._createSchemas = function(schemaConfig) {
schemaConfig = schemaConfig || {};
return Object.keys(schemaConfig).reduce(function(cache, key) {
var spec = schemaConfig[key];
+ // All resources have an autoincrementing index so we can order them suckas
+ spec.__index = {type: 'meta', autoincrement: true};
cache[key] = OKSchema(spec);
return cache;
}, {});
@@ -248,7 +253,7 @@ function ResourceCache(resources) {
throw new Error('Undefined resource given to ResourceCache');
if (resource.bound) {
cache[resource.type] = resource.parent;
- cache[resource.type + ':' + resource.id] = resource;
+ cache[resource.type + ':' + resource.getID()] = resource;
} else {
cache[resource.type] = resource;
}
diff --git a/app/node_modules/okadminview/index.js b/app/node_modules/okadminview/index.js
index a376df5..897c583 100644
--- a/app/node_modules/okadminview/index.js
+++ b/app/node_modules/okadminview/index.js
@@ -1,6 +1,9 @@
-var assign = require('object-assign')
+var assign = require('object-assign');
+var cloneDeep = require('lodash.clonedeep');
var bodyParser = require('body-parser');
var methodOverride = require('method-override');
+var session = require('express-session');
+var flash = require('connect-flash');
var Q = require('q');
var pluralize = require('pluralize');
var OKQuery = require('okquery');
@@ -22,12 +25,14 @@ function OKAdminView(options) {
throw new Error('No templateProvider provided to OKAdminView');
if (!options.meta)
throw new Error('No meta query provided to OKAdminView');
+
var app = options.app;
var express = options.express;
var meta = options.meta;
var resourceCache = this._resourceCache = options.resourceCache;
- var resourceConfig = this._resourceConfig = options.resourceConfig;
+ var resourceConfig = this._resourceConfig = cloneDeep(options.resourceConfig);
var provider = options.templateProvider;
+
// Load templates
var templates = this._templates =
['index', 'resource', 'resource_new'].reduce(function(cache, name) {
@@ -37,6 +42,7 @@ function OKAdminView(options) {
cache[name] = template;
return cache;
}, {});
+
// OKAdmin middleware is a router, so mounts on 'use'
Object.defineProperty(this, 'mount', {
value: 'use',
@@ -51,13 +57,14 @@ function OKAdminView(options) {
var config = resourceConfig[key];
var type = config.type;
var staticData = config.static || {};
+ // Get parent level resource
var resource = resourceCache.get(config.type);
if (!resource)
throw new Error('Something weird is going on');
- var id = staticData[resource.idField];
+ var id = resource.getID(staticData);
+ // Check to see if there's a more specific instance
resource = resourceCache.get(type, id) || resource;
if (resource.bound) {
- // Resource instances implement the query API
return OKQuery({resource: resource});;
} else {
return OKQuery({resource: resource, query: config.query})
@@ -71,6 +78,14 @@ function OKAdminView(options) {
strict: app.get('strict routing')
});
+ // Enable basic sessions for flash messages
+ router.use(session({
+ secret: 'okadmin',
+ resave: false,
+ saveUninitialized: false
+ }));
+ // Enable flash messaging
+ router.use(flash());
// Parse form data
router.use(bodyParser.urlencoded({extended: true}));
// HTML forms only support POST and GET methods
@@ -87,7 +102,10 @@ function OKAdminView(options) {
router.get('/', function readIndex(req, res, next) {
fetchIndexTemplateData(meta, indexQueries).then(function(data) {
- view.renderIndex(req, res, data);
+ view.renderIndex(req, res, assign(data, {
+ success: req.flash('success'),
+ errors: req.flash('errors')
+ }));
}).fail(errorHandler(req, res));
});
@@ -98,13 +116,11 @@ function OKAdminView(options) {
errorHandler(req, res)(new Error('No such resource ' + type));
} else {
meta.get().then(function(metadata) {
- view.renderResourceNew(req, res, {
- meta: metadata,
- resource: {
- type: resource.type,
- spec: resource.spec
- }
- });
+ var templateData = getResourceTemplateData(metadata, resource, {});
+ view.renderResourceNew(req, res, assign(templateData, {
+ success: req.flash('success'),
+ errors: req.flash('errors'),
+ }));
}).fail(errorHandler(req, res));
}
});
@@ -120,13 +136,17 @@ function OKAdminView(options) {
resource: resource,
query: id
});
- fetchResourceTemplateData(meta, query, getResourceTemplateData).then(function(data) {
- if (!data) {
- resourceMissingHandler(req, res)()
- } else {
- view.renderResource(req, res, data);
- }
- }).fail(errorHandler(req, res));
+ fetchResourceTemplateData(meta, query, getResourceTemplateData)
+ .then(function(data) {
+ if (!data) {
+ resourceMissingHandler(req, res)()
+ } else {
+ view.renderResource(req, res, assign(data, {
+ success: req.flash('success'),
+ errors: req.flash('errors')
+ }));
+ }
+ }).fail(errorHandler(req, res));
}
});
@@ -142,7 +162,8 @@ function OKAdminView(options) {
try {
resource.assertValid(data);
resource.create(data).then(function(created) {
- res.redirect(303, data[resource.idField]);
+ req.flash('success', {action: 'create'});
+ res.redirect(303, resource.getID(data));
}).fail(errorHandler(req, res));
} catch (errors) {
var templateData = getResourceTemplateData(metadata, resource, data);
@@ -152,6 +173,40 @@ function OKAdminView(options) {
}
});
+ router.put('/:type/__batch/', function putBatch(req, res, next) {
+ var type = req.params.type;
+ var body = req.body || {};
+ var resourcesJSON = body[type];
+ var resource = resourceCache.get(type);
+ if (!resourcesJSON || !resourcesJSON.length) {
+ res.status(400);
+ errorHandler(req, res)(new Error('Bad request'));
+ } else if (!resource) {
+ res.status(404);
+ errorHandler(req, res)(new Error('No such resource'));
+ } else {
+ try {
+ var ids = [];
+ var resourcesParsed = resourcesJSON.map(function(resourceJSON) {
+ var data = JSON.parse(resourceJSON);
+ ids.push(resource.getID(data));
+ return data;
+ });
+ } catch (e) {
+ errorHandler(req, res)(new Error('Resource batch contains invalid JSON'));
+ return;
+ }
+ Q.all([
+ meta.get(),
+ resource.updateBatch(ids, resourcesParsed),
+ ]).then(function(results) {
+ var metadata = results.shift();
+ req.flash('success', {action: 'batch_update'});
+ res.redirect(303, '../..');
+ }).fail(errorHandler(req, res));
+ }
+ });
+
router.put('/:type/:id/', function updateResource(req, res, next) {
var type = req.params.type;
var id = req.params.id;
@@ -165,7 +220,8 @@ function OKAdminView(options) {
try {
resource.assertValid(data);
resource.update(id, data).then(function(updated) {
- res.redirect(303, '../' + updated[resource.idField]);
+ req.flash('success', {action: 'update'});
+ res.redirect(303, '../' + resource.getID(updated));
}).fail(errorHandler(req, res));
} catch (errors) {
var templateData = getResourceTemplateData(metadata, resource, data);
@@ -184,6 +240,7 @@ function OKAdminView(options) {
} else {
meta.get().then(function(metadata) {
resource.destroy(id).then(function() {
+ req.flash('success', {action: 'delete'});
res.redirect(303, '../..');
}).fail(errorHandler(req, res));
}).fail(errorHandler(req, res));
@@ -203,13 +260,19 @@ function getResourceTemplateData(meta, resource, data) {
data = data || {};
var spec = Object.keys(resource.spec).reduce(function(cache, prop) {
var value = data[prop];
- cache[prop].value = value;
+ var propSpec = cache[prop];
+ // Decorate spec with actual resource values
+ propSpec.value = value;
+ // Some fields should not be shown to the user
+ if (propSpec.type === 'meta' || propSpec.static) {
+ propSpec.hidden = true;
+ }
return cache;
}, resource.spec);
return {
meta: meta,
resource: {
- id: data[resource.idField],
+ id: resource.getID(data),
type: resource.type,
spec: spec
}
@@ -245,7 +308,7 @@ OKAdminView.prototype.renderResourceNew = function(req, res, data) {
* Annotate template data with schema info
*/
function fetchIndexTemplateData(meta, queries) {
- return Q.Promise(function(resolve, reject) {
+ return Q.promise(function(resolve, reject) {
Q.all([meta.get()].concat(queries.map(function(query) {
return query.get();
}))).then(function(results) {
@@ -266,14 +329,14 @@ function fetchIndexTemplateData(meta, queries) {
}
if (result.length) {
- result.forEach(addToCache)
+ result.forEach(addData)
} else {
- addToCache(result);
+ addData(result);
}
- function addToCache(data) {
+ function addData(data) {
// Report id to template under standard name
- data.id = data[resource.idField];
+ data.id = resource.getID(data);
cache[key].data.push(data);
}
@@ -293,7 +356,7 @@ function fetchIndexTemplateData(meta, queries) {
*/
function fetchResourceTemplateData(meta, query, fn) {
fn = fn || function(m, r, d) { return {meta: m, resource: d}; };
- return Q.Promise(function(resolve, reject) {
+ return Q.promise(function(resolve, reject) {
meta.get().then(function(metadata) {
query.get().then(function(data) {
var resource = query.resource;
diff --git a/app/node_modules/okadminview/package.json b/app/node_modules/okadminview/package.json
index 4832db1..4c6d11c 100644
--- a/app/node_modules/okadminview/package.json
+++ b/app/node_modules/okadminview/package.json
@@ -10,6 +10,9 @@
"license": "None",
"dependencies": {
"body-parser": "^1.12.2",
+ "connect-flash": "^0.1.1",
+ "express-session": "^1.11.1",
+ "lodash.clonedeep": "^3.0.0",
"method-override": "^2.3.2",
"object-assign": "^2.0.0",
"pluralize": "^1.1.2",
diff --git a/app/node_modules/okdb/index.js b/app/node_modules/okdb/index.js
index 4be646d..2cf6d8b 100644
--- a/app/node_modules/okdb/index.js
+++ b/app/node_modules/okdb/index.js
@@ -1,6 +1,10 @@
-var Q = require('q');
+var assign = require('object-assign')
+var cloneDeep = require('lodash.clonedeep');
+var isobject = require('lodash.isobject');
var format = require('util').format;
var low = require('lowdb');
+var Q = require('q');
+
low.mixin(low.mixin(require('underscore-db')));
/**
@@ -10,14 +14,14 @@ low.mixin(low.mixin(require('underscore-db')));
function OKDB(options) {
if (!(this instanceof OKDB)) return new OKDB(options);
options = options || {};
- var type;
+ var db;
if (typeof options === 'string')
- type = options;
+ db = options;
else
- type = options.type;
- if (!type)
- throw new Error('No DB type provided');
- switch (type) {
+ db = options.db;
+ if (!db)
+ throw new Error('No DB db provided to OKDB');
+ switch (db) {
case 'fs':
return FSDB(options);
default:
@@ -27,88 +31,182 @@ function OKDB(options) {
/**
* DB implementation backed by a JSON file.
- * TODO Incomplete
*/
function FSDB(options) {
if (!(this instanceof FSDB)) return new FSDB(options);
options = options || {};
+ if (!options.schemas)
+ throw new Error('No schemas provided to FSDB')
+ this._schemas = options.schemas;
var name = options.name || 'db';
var filename = name + '.json';
this._db = low(filename);
}
-FSDB.prototype._resolve = function(data, error) {
- return Q.Promise(function resolvePromise(resolve, reject) {
- if (error) {
- reject(error);
- } else {
- resolve(data);
- }
- });
+FSDB.prototype.get = function(collection, id) {
+ var schema = this._schemas[collection];
+ if (!schema)
+ return resolve(null, new Error('No such collection type'));
+
+ var query = getQuery(schema, id);
+ if (!query)
+ return resolve(null, new Error('Bad query'));
+
+ var result = this._db(collection).find(query);
+ return resolve(result ? cloneDeep(result) : result);
};
-FSDB.prototype.get = function(collection, query) {
- if (!query) {
- return this._resolve(null, new Error('No query given'));
+/**
+ * Add a new document to the DB
+ */
+FSDB.prototype.insert = function(collection, data) {
+ var schema = this._schemas[collection];
+ var wrapped = this._db(collection);
+ if (!schema)
+ return resolve(null, new Error('No such collection type'));
+ // Get detached, clone deep, data sleep, beep beep
+ data = cloneDeep(data);
+ // Auto-increment fields
+ data = autoincrement(wrapped, schema, data);
+ var result = wrapped.chain().push(data).last().value();
+
+ if (result) {
+ return resolve(cloneDeep(result));
} else {
- var data = this._db(collection).find(query);
- return this._resolve(data);
+ return resolve(null, new Error('Problem inserting document'));
}
};
-FSDB.prototype.put = function(collection, query, data) {
- data = data || {};
- if (!query) {
- return this._resolve(null, new Error('No query given'));
- } else if (this._db(collection).find(query)) {
- var updated = this._db(collection)
- .chain()
- .find(query)
- .assign(data)
- .value();
- return this._resolve(updated);
+/**
+ * Update an existing document in the DB
+ */
+FSDB.prototype.update = function(collection, id, data) {
+ var schema = this._schemas[collection];
+ if (!schema)
+ return resolve(null, new Error('No such collection type'));
+
+ var query = getQuery(schema, id);
+ var wrapped = this._db(collection);
+ var chain = wrapped.chain().find(query);
+
+ if (!chain.value()) {
+ return resolve(null, new Error('Cannot update nonexistent entry'));
+ }
+
+ var result = chain.assign(cloneDeep(data)).value();
+ if (result ) {
+ return resolve(cloneDeep(result));
} else {
- return this._resolve(null, new Error('Cannot update nonexistent entry'));
+ return resolve(null, new Error('Problem updating document'));
}
};
-FSDB.prototype.create = function(collection, data) {
- var created = this._db(collection)
- .chain()
- .push(data)
- .last()
- .value();
- return this._resolve(created);
+/**
+ * TODO Should be atomic ¯\_(ツ)_/¯
+ */
+FSDB.prototype.updateBatch = function(collection, ids, datas) {
+ var schema = this._schemas[collection];
+ if (!schema)
+ return resolve(null, new Error('No such collection type'));
+ if (!ids || !ids.length || !datas || !datas.length ||
+ ids.length !== datas.length) {
+ return resolve(null, new Error('Bad input'));
+ }
+
+ var doc = this._db(collection);
+ var results = ids.map(function(id, i) {
+ return doc.chain().find(getQuery(schema, id)).assign(datas[i]).value();
+ });
+
+ return resolve(results);
};
-FSDB.prototype.remove = function(collection, query) {
- if (!collection || !query) {
- return this._resolve(null, new Error('Bad input'));
+FSDB.prototype.remove = function(collection, id) {
+ var schema = this._schemas[collection];
+ if (!schema)
+ return resolve(null, new Error('No such collection type'));
+
+ var query = getQuery(schema, id);
+ var result = this._db(collection).removeWhere(query);
+
+ if (result) {
+ // Don't need to clone this ref, since it's removed anyway
+ return resolve(result);
} else {
- var data = this._db(collection).removeWhere(query);
- if (!data)
- var error = new Error('Cannot remove nonexistent entry');
- return this._resolve(data, error);
+ return resolve(null, new Error('Cannot remove nonexistent entry'));
}
};
-FSDB.prototype.find = function(collection, query) {
- if (!collection || !query) {
- return this._resolve(null, new Error('Bad input'));
- } else {
- var data = this._db(collection).find(query);
- return this._resolve(data);
- }
+FSDB.prototype.sortBy = function(collection, prop, descend) {
+ var schema = this._schemas[collection];
+ if (!schema)
+ return resolve(null, new Error('No such collection type'));
+ if (!prop)
+ return resolve(null, new Error('Bad input'));
+
+ var result = this._db(collection).sortByOrder([prop], [!descend]);
+ return resolve(result);
+};
+
+FSDB.prototype.find = function(collection, id) {
+ var schema = this._schemas[collection];
+ if (!schema)
+ return resolve(null, new Error('No such collection type'));
+
+ var query = getQuery(schema, id);
+ var result = this._db(collection).find(query);
+
+ return resolve(cloneDeep(result));
+};
+
+FSDB.prototype.all = function(collection) {
+ var schema = this._schemas[collection];
+ if (!schema)
+ return resolve(null, new Error('No such collection type'));
+
+ var data = this._db(collection).value();
+ return resolve(cloneDeep(data || []));
};
FSDB.prototype.getMeta = function() {
var data = this._db('meta').first();
- return this._resolve(data || {});
+ return resolve(data || {});
};
-FSDB.prototype.getAll = function(collection) {
- var data = this._db(collection).toArray();
- return this._resolve(data || []);
+/**
+ * Function implementing DB auto increment support
+ * Naive implementation, assumes DB is relatively small.
+ */
+function autoincrement(wrapper, schema, data) {
+ return schema.autoIncrementFields.reduce(function(data, field) {
+ var last = wrapper.chain().sort(field).first().value();
+ var index = last ? last[field] : -1;
+ var incremented = {};
+ incremented[field] = (parseInt(index) + 1);
+ return assign(data, incremented);
+ }, data);
+}
+
+function getQuery(schema, id) {
+ if (schema && id) {
+ var query = {};
+ query[schema.idField] = id;
+ return query;
+ }
+}
+
+/**
+ * Helper function to create promises for DB data
+ */
+function resolve(data, error) {
+ return Q.promise(function resolvePromise(resolve, reject) {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(data);
+ }
+ });
};
+
module.exports = OKDB;
diff --git a/app/node_modules/okdb/package.json b/app/node_modules/okdb/package.json
index 659422e..5184cb6 100644
--- a/app/node_modules/okdb/package.json
+++ b/app/node_modules/okdb/package.json
@@ -9,7 +9,10 @@
"author": "OKFocus",
"license": "None",
"dependencies": {
+ "lodash.clonedeep": "^3.0.0",
+ "lodash.isobject": "^3.0.1",
"lowdb": "^0.7.2",
+ "object-assign": "^2.0.0",
"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
index 2d93e2a..9cc8b78 100644
--- a/app/node_modules/okquery/index.js
+++ b/app/node_modules/okquery/index.js
@@ -1,3 +1,4 @@
+var cloneDeep = require('lodash.clonedeep');
var assign = require('object-assign');
var isobject = require('lodash.isobject');
var Q = require('q');
@@ -16,6 +17,9 @@ function OKQuery(options) {
var resource = options.resource;
var type = resource.type;
var query = options.query || '*';
+ // Ensure immutability
+ if (isobject(query))
+ query = cloneDeep(query);
Object.defineProperty(this, 'resource', {
value: resource,
@@ -55,6 +59,9 @@ function createQuery(resource, query, options) {
function queryComplex(resource, query) {
var dynamicProp;
+ // Query is an object specifying key value pairs against which
+ // to match DB entries. Iterate through and check if any of the values
+ // is unbound e.g. :id
var notDynamic = Object.keys(query).every(function(prop) {
var matcher = query[prop];
if (isDynamic(matcher)) {
@@ -71,6 +78,8 @@ function queryComplex(resource, query) {
}
} else {
return function(id) {
+ // Bind the dynamic property to its value
+ // and add the pair to the query
var dynamicQuery = {};
dynamicQuery[dynamicProp] = id;
var query = assign({}, query, dynamicQuery);
@@ -87,7 +96,8 @@ function queryDynamic(resource) {
function queryAll(resource) {
return function() {
- return resource.all();
+ // Always return sorted results
+ return resource.sortBy('__index');
};
}
diff --git a/app/node_modules/okquery/package.json b/app/node_modules/okquery/package.json
index 606d45b..5ba9dd5 100644
--- a/app/node_modules/okquery/package.json
+++ b/app/node_modules/okquery/package.json
@@ -9,6 +9,7 @@
"author": "OKFocus",
"license": "None",
"dependencies": {
+ "lodash.clonedeep": "^3.0.0",
"lodash.isobject": "^3.0.1",
"object-assign": "^2.0.0",
"q": "^1.2.0"
diff --git a/app/node_modules/okresource/index.js b/app/node_modules/okresource/index.js
index 0e8498f..c3f9adb 100644
--- a/app/node_modules/okresource/index.js
+++ b/app/node_modules/okresource/index.js
@@ -20,17 +20,6 @@ function OKResource(options) {
var schema = options.schema;
var spec = schema.spec;
- // Iterate through spec to find field which will act as the
- // resource id in da db.
- var idField = Object.keys(spec).reduce(function(idField, prop) {
- var propSpec = spec[prop];
- if (propSpec.id)
- idField = prop;
- return idField;
- // If schema has a prop called 'id', default to that one
- }, spec.id && 'id');
- if (!idField)
- throw new Error('Bad schema: no ID field');
var type = options.type;
this._db = options.db;
@@ -51,12 +40,6 @@ function OKResource(options) {
enumerable: true
});
- Object.defineProperty(this, 'idField', {
- value: idField,
- writable: false,
- enumerable: true
- });
-
// Whether this resource represents a specific data point
// or a whole class of data
Object.defineProperty(this, 'bound', {
@@ -74,19 +57,22 @@ OKResource.prototype.assertValid = function(data) {
};
OKResource.prototype.all = function() {
- return this._db.getAll(this.type);
+ return this._db.all(this.type);
};
-OKResource.prototype.create = function(data) {
+OKResource.prototype.getID = function(data) {
data = data || {};
+ return data[this._schema.idField];
+};
+
+OKResource.prototype.create = function(data) {
var type = this.type;
var db = this._db;
- var id = data[this.idField];
return Q.promise(function(resolve, reject) {
- if (!id) {
- reject(new Error('Data does not contain ID property'));
+ if (!data) {
+ reject(new Error('No data provided'));
} else {
- db.create(type, data).then(resolve).fail(reject);
+ db.insert(type, data).then(resolve).fail(reject);
}
});
};
@@ -94,13 +80,11 @@ OKResource.prototype.create = function(data) {
OKResource.prototype.destroy = function(id) {
var db = this._db;
var type = this.type;
- var query = {};
- query[this.idField] = id;
return Q.promise(function(resolve, reject) {
if (!id) {
- reject(new Error('No ID given'));
+ reject(new Error('No ID provided'));
} else {
- db.remove(type, query).then(resolve).fail(reject);
+ db.remove(type, id).then(resolve).fail(reject);
}
});
};
@@ -111,7 +95,7 @@ OKResource.prototype.find = function(query) {
var type = this.type;
return Q.promise(function(resolve, reject) {
if (!query) {
- throw new Error('No query given');
+ throw new Error('No query provided');
} else {
db.find(type, query).then(resolve).fail(reject);
}
@@ -121,53 +105,65 @@ OKResource.prototype.find = function(query) {
OKResource.prototype.get = function(id) {
var db = this._db;
var type = this.type;
- var idField = this.idField;
return Q.promise(function(resolve, reject) {
if (!id) {
- throw new Error('No ID given');
+ throw new Error('No ID provided');
} else {
- // We have the id, but we still need
- // to resolve which field is the id field
- // to match
- var query = {};
- query[idField] = id;
- db.get(type, query).then(resolve).fail(reject);
+ db.get(type, id).then(resolve).fail(reject);
}
});
};
OKResource.prototype.update = function(id, data) {
- data = data || {};
var db = this._db;
var type = this.type;
- var idField = this.idField;
return Q.promise(function(resolve, reject) {
if (!id) {
reject(new Error('No resource ID provided'));
+ } else if (!data) {
+ reject(new Error('No data provided'));
} else {
- var query = {};
- query[idField] = id;
- db.put(type, query, data).then(resolve).fail(reject);;
+ db.update(type, id, data).then(resolve).fail(reject);;
}
});
};
+OKResource.prototype.updateBatch = function(ids, datas) {
+ var self = this;
+ var db = this._db;
+ var type = this.type;
+ return Q.promise(function(resolve, reject) {
+ if (!ids || !ids.length || !datas || !datas.length ||
+ ids.length !== datas.length) {
+ reject(new Error('Bad input'));
+ } else {
+ db.updateBatch(type, ids, datas).then(resolve).fail(reject);
+ }
+ });
+}
+
+
+/**
+ * Get all documents in collection sorted by property,
+ * optionally in descending order
+ */
+OKResource.prototype.sortBy = function(prop, descend) {
+ return this._db.sortBy(this.type, prop, descend);
+};
+
OKResource.prototype.updateOrCreate = function(id, data) {
data = data || {};
var type = this.type;
var db = this._db;
- var idField = this.idField;
- var query = {};
- query[idField] = id;
return Q.promise(function(resolve, reject) {
if (!id) {
- reject(new Error('No resource ID provided'));
+ reject(new Error('No ID provided'));
} else {
- db.get(type, query).then(function(persisted) {
+ db.get(type, id).then(function(persisted) {
if (persisted) {
- db.put(type, query, data).then(resolve).fail(reject);
+ db.update(type, id, data).then(resolve).fail(reject);
} else {
- db.create(type, data).then(resolve).fail(reject);
+ db.insert(type, data).then(resolve).fail(reject);
}
}).fail(reject);
}
@@ -196,11 +192,15 @@ function OKResourceInstance(resource, options) {
// configuration, but may not actually be present
// in the database and need custom logic to handle this.
var staticData = cloneDeep(options.static);
- var id = staticData[resource.idField];
+ var id = resource.getID(staticData);
if (!id)
throw new Error(
'Cannot create static OKResourceInstance without an ID field');
+ this.getID = function() {
+ return id;
+ };
+
/**
* Ensure that static data is provided on get
*/
@@ -209,9 +209,9 @@ function OKResourceInstance(resource, options) {
resource.get(id).then(function(data) {
// Note the assign call. Don't expose private references!
if (data) {
- resolve(assign({}, data, cloneDeep(staticData)));
+ resolve(assign(data, cloneDeep(staticData)));
} else {
- resolve(assign({}, cloneDeep(staticData)));
+ resolve(cloneDeep(staticData));
}
}).fail(reject);
});
@@ -286,14 +286,9 @@ function OKResourceInstance(resource, options) {
});
Object.defineProperty(this, 'spec', {
- value: resource.spec,
- writable: false,
- enumerable: true
- });
-
- Object.defineProperty(this, 'id', {
- value: id,
- writable: false,
+ get: function() {
+ return resource.spec
+ },
enumerable: true
});
@@ -303,23 +298,11 @@ function OKResourceInstance(resource, options) {
enumerable: true
});
- Object.defineProperty(this, 'idField', {
- value: resource.idField,
- writable: false,
- enumerable: true
- });
-
Object.defineProperty(this, 'bound', {
value: true,
writable: false,
enumerable: true
});
-
- Object.defineProperty(this, 'class', {
- value: resource,
- writable: false,
- enumerable: true
- });
}
module.exports = OKResource;
diff --git a/app/node_modules/okschema/index.js b/app/node_modules/okschema/index.js
index 4b215d1..d53ed7b 100644
--- a/app/node_modules/okschema/index.js
+++ b/app/node_modules/okschema/index.js
@@ -56,6 +56,11 @@ var types = {
}];
}
}
+ },
+ // Special type for resource meta information
+ 'meta': {
+ parent: 'string',
+ assertValid: function(spec, value) {}
}
}
@@ -69,8 +74,9 @@ function OKSchema(spec) {
if (!spec)
throw new Error('No spec provided to OKSchema');
spec = cloneDeep(spec);
+ var specKeys = Object.keys(spec);
// Cache the mschema version of our spec
- this._mschemaSpec = Object.keys(spec).reduce(function(cache, prop) {
+ this._mschemaSpec = specKeys.reduce(function(cache, prop) {
// If custom type, return its parent spec
var type = spec[prop].type;
if (types[type]) {
@@ -82,12 +88,46 @@ function OKSchema(spec) {
return cache;
}, {});
+ // Find ID field
+ var idField;
+ specKeys.every(function(prop) {
+ if (prop === 'id' || spec[prop].id) {
+ idField = prop;
+ return false;
+ } else {
+ return true;
+ }
+ });
+
+ // Register autoincrement fields
+ // NOTE Does not work for nested fields
+ var autoIncrementFields = specKeys.reduce(function(arr, prop) {
+ var specProp = spec[prop];
+ if (specProp.autoincrement) {
+ arr.push(prop);
+ }
+ return arr;
+ }, []);
+
Object.defineProperty(this, 'spec', {
get: function() {
return cloneDeep(spec);
},
enumerable: true
});
+
+ Object.defineProperty(this, 'idField', {
+ value: idField,
+ writable: true,
+ enumerable: true
+ });
+
+ Object.defineProperty(this, 'autoIncrementFields',{
+ get: function() {
+ return cloneDeep(autoIncrementFields);
+ },
+ enumerable: true
+ });
}
OKSchema.prototype.assertValid = function(data) {
diff --git a/app/node_modules/okserver/index.js b/app/node_modules/okserver/index.js
index 1645eaa..cf06b3c 100644
--- a/app/node_modules/okserver/index.js
+++ b/app/node_modules/okserver/index.js
@@ -67,16 +67,6 @@ function OKServer(options) {
// Make sure this lady is last. Checks whether the desired
// route has a trailing-slash counterpart and redirects there
app.use(slash());
-
- /**
- * Create a handler which redirect all requests to
- * the same route with a trailing slash appended
- */
- function redirect(routeNoSlash) {
- return function(req, res) {
- res.redirect(301, routeNoSlash + '/');
- }
- }
}
OKServer.prototype.listen = function listen(port) {
diff --git a/app/node_modules/oktemplate/index.js b/app/node_modules/oktemplate/index.js
index a37f78e..f2c2e07 100644
--- a/app/node_modules/oktemplate/index.js
+++ b/app/node_modules/oktemplate/index.js
@@ -14,13 +14,24 @@ var filters = {
* Return a string formatted version of a JSON object.
* Useful for quick debugging of template data.
*/
- stringify: function(obj) {
+ prettify: function(obj) {
try {
return '<pre>' + stringify(obj) + '</pre>';
} catch (e) {
+ return 'Error prettifying';
+ }
+ },
+
+ /**
+ * Serialize Javascript objects into a JSON string
+ */
+ stringify: function(obj) {
+ try {
+ return JSON.stringify(obj);
+ } catch (e) {
return 'Error stringifying';
}
- }
+ },
};
diff --git a/app/node_modules/okview/index.js b/app/node_modules/okview/index.js
index 6eebe6e..2d1c0a0 100644
--- a/app/node_modules/okview/index.js
+++ b/app/node_modules/okview/index.js
@@ -159,10 +159,10 @@ function fetchTemplateData(meta, queries, id) {
// Inform template of ID in generic field
if (manyResult) {
result = result.map(function(data) {
- return assign({}, data, {id: data[resource.idField]})
+ return assign({}, data, {id: resource.getID(data)})
});
} else {
- result = assign({}, result, {id: result[resource.idField]});
+ result = assign({}, result, {id: resource.getID(result)});
}
// If we have a lot of results for a certain type,
// we pluralize the key and yield an array of results
diff --git a/examples/db.json b/examples/db.json
index 148391a..20d4510 100644
--- a/examples/db.json
+++ b/examples/db.json
@@ -3,8 +3,8 @@
"bread": [
{
"type": "pretzel",
- "description": "really a very tasty bread! yup yes",
- "color": "green",
+ "description": "really a very tasty bread!",
+ "color": "red",
"id": "pretzel",
"title": "Pretzel Chips",
"images": [
@@ -23,7 +23,8 @@
"token": "",
"title": "",
"thumb": ""
- }
+ },
+ "__index": 0
},
{
"type": "bagel",
@@ -38,31 +39,73 @@
"token": "112498725",
"title": "FW14-2H-VIDEO-V4 2",
"thumb": "http://i.vimeocdn.com/video/497493142_640.jpg"
- }
+ },
+ "__index": 1
},
{
"type": "pumpernickel",
- "description": "grandma's recipe",
+ "description": "yup",
"id": "pumpernickel",
- "title": "Pumpernickel",
+ "title": "ok",
"images": [
{
"uri": "cool",
"caption": "cool"
}
- ]
+ ],
+ "color": "red",
+ "video": {
+ "url": "",
+ "type": "",
+ "token": "",
+ "title": "",
+ "thumb": ""
+ },
+ "__index": 2
+ },
+ {
+ "type": "cracker",
+ "title": "",
+ "description": "once upon a time this noble creature etc",
+ "color": "red",
+ "video": {
+ "url": "",
+ "type": "",
+ "token": "",
+ "title": "",
+ "thumb": ""
+ },
+ "__index": 4,
+ "id": "cracker"
+ },
+ {
+ "type": "croissant",
+ "title": "",
+ "description": "wow just wow",
+ "color": "red",
+ "video": {
+ "url": "",
+ "type": "",
+ "token": "",
+ "title": "",
+ "thumb": ""
+ },
+ "__index": 3,
+ "id": "croissant"
}
],
"page": [
{
"title": "About Us",
"body": "Just a small bakery",
- "id": "about"
+ "id": "about",
+ "__index": 1
},
{
- "title": "contact",
+ "title": "ok...",
"body": "2406 Old Rd, San Juan Bautista",
- "id": "contact"
+ "id": "contact",
+ "__index": 0
}
]
} \ No newline at end of file
diff --git a/site/db.json b/site/db.json
index 8ee246c..c07e321 100644
--- a/site/db.json
+++ b/site/db.json
@@ -27,24 +27,24 @@
"uri": "http://twohustlers.com/bigimages/ss15_3.jpg",
"caption": "CURABITUR BLANDIT TEMPUS PORTTITOR 4"
}
- ]
+ ],
+ "__index": 1
}
],
- "advertising": [],
- "experiential": [],
- "content": [],
"page": [
{
"id": "about",
"title": "WHO WE ARE",
"body": "WE ARE A COLLECTIVE OF DIGITAL NATIVES, INDUSTRY INFLUENCERS, TRENDSETTERS, MILLENNIAL COMMUNICATORS & UNORTHODOX STRATEGISTS – A NEW MEDIA CREATIVE LAB. TOGETHER WE CREATE LIVING, BREATHING, CONTENT DRIVEN EXPERIENCES THAT ENGAGE TODAY’S CONSUMER AUDIENCE. WE ARE WILD, PUNK, PROVOCATIVE, RADICAL, FEARLESS, SUBVERSIVE, BOISTEROUS, ANARCHIC, UNEXPECTED & DISRUPTIVE. MOST OF ALL WE ARE CURIOUS. WELCOME TO THE WORLD OF TWO HUSTLERS.",
- "image": "http://www.lansdowneresort.com/meetings/assets/images/masthead/meetings-team-building.jpg"
+ "image": "http://www.lansdowneresort.com/meetings/assets/images/masthead/meetings-team-building.jpg",
+ "__index": 1
},
{
"id": "contact",
"title": "CONTACT",
"body": "TWOHUSTLERS\r\n50 WHITE STREET\r\nNEW YORK, NY 10013\r\n<a href=\"mailto:info@twohustlers.com\">INFO@TWOHUSTLERS.COM</a>\r\n+1 646 370-1180\r\nTWOHUSTLERS ©2014\r\nWEBSITE BY <a href=\"http://okfoc.us/\">OKFOCUS</a>",
- "image": "http://checkingintocollege.com/wp/wp-content/uploads/2014/08/angryphone.jpg"
+ "image": "http://checkingintocollege.com/wp/wp-content/uploads/2014/08/angryphone.jpg",
+ "__index": 0
}
]
-} \ No newline at end of file
+}
diff --git a/themes/okadmin/public/css/main.css b/themes/okadmin/public/css/main.css
index ad940e8..3762fd4 100644
--- a/themes/okadmin/public/css/main.css
+++ b/themes/okadmin/public/css/main.css
@@ -14,11 +14,12 @@ html, body {
background-attachment: scroll;
}
-ul {
+ul, ol {
padding: 0;
list-style: none;
}
+.main.index .resource-category button,
a {
color: #A200FF;
text-decoration: none;
@@ -26,6 +27,7 @@ a {
text-transform: uppercase;
}
+.main.index .resource-category button:hover,
a:hover {
border-bottom: 1px solid #A200FF;
}
@@ -65,7 +67,12 @@ a:visited {
border: 2px solid #ddd;
}
-nav {
+.main.index .resource-category.active li:before {
+ content: "፧";
+ margin-right: 1em;
+}
+
+.resource-nav {
background: white;
width: 10%;
margin: 2.5em 1em;
@@ -89,21 +96,63 @@ h2 {
transform: rotate(-1deg);
}
-.main.index .resource-category a.add-new {
- border-bottom: 3px solid rgba(0, 0, 0, 0);
+.main.index .resource-category nav {
float: right;
- font-size: 1.5em;
+}
+
+.main.index .resource-category.active ol {
+ cursor: -webkit-grab;
+ cursor: grab;
+}
+
+.main.index .resource-category.active li a {
+ pointer-events: none;
+}
+
+ /* Makes the button look like a link */
+.main.index .resource-category button {
+ background: none !important;
+ height: 1.5em;
+ border: none;
+ padding: 0 !important;
+ font: inherit;
+ cursor: pointer;
+}
+
+.main.index .resource-category .btn {
+ border-bottom: 3px solid rgba(0, 0, 0, 0);
color: rgba(0, 0, 0, 0.25);
+ line-height: 20px;
}
-.main.index .resource-category li {
- margin: 1em 0;
+.main.index .resource-category .btn {
+ display: none;
+}
+
+.main.index .resource-category .btn.active {
+ display: inline;
}
-.main.index .resource-category a.add-new:hover {
+.main.index .resource-category .btn:hover {
border-bottom: 3px solid rgba(0, 0, 0, 0.25);
}
+.main.index .resource-category .btn {
+ margin-right: 1em;
+}
+
+.main.index .resource-category .btn:last-child {
+ margin-right: 0;
+}
+.main.index .resource-category .add-btn {
+ font-size: 20px;
+}
+
+.main.index .resource-category li {
+ margin: 1em 0;
+}
+
+
.main.resource {
float: left;
margin-top: 2em;
@@ -278,3 +327,7 @@ li.image-element .remove-image:hover {
.clear {
clear: both;
}
+
+.hidden {
+ display: none;
+}
diff --git a/themes/okadmin/public/js/app.js b/themes/okadmin/public/js/app.js
index 441172f..1ab9956 100644
--- a/themes/okadmin/public/js/app.js
+++ b/themes/okadmin/public/js/app.js
@@ -50,10 +50,10 @@ var OKAdmin = function(){
}))
// fix post indexing in list-driven inputs
- $("form").submit(function(){
+ $(".main.resource form").submit(function(){
$(".image-element").each(function(index){
$(this).find("input,textarea").each(function(){
- var field = $(this).attr("name").replace(/\[\]/, "[" + index + "]")
+ var field = $(this).attr("name").replace(/\[[0-9]*\]/, "[" + index + "]")
$(this).attr("name", field)
})
})
@@ -67,6 +67,47 @@ var OKAdmin = function(){
e.preventDefault()
}
})
+
+ $(".resource-category").on("click", ".edit-btn", function(e) {
+ e.preventDefault();
+ var $parent = $(e.delegateTarget);
+ var $editBtn = $parent.find(".edit-btn");
+ var $cancelBtn = $parent.find(".cancel-btn");
+ var $saveBtn = $parent.find(".save-btn");
+ var $ol = $parent.find("ol");
+ var toggles = [$parent, $cancelBtn, $saveBtn, $editBtn];
+
+ $ol.sortable();
+ $ol.disableSelection();
+ toggle();
+
+ $cancelBtn.one("click", function(e) {
+ $ol.sortable("cancel");
+ $ol.enableSelection();
+ toggle();
+ });
+
+ $saveBtn.one("click", function(e) {
+ $ol.sortable();
+ toggle();
+ });
+
+ function toggle() {
+ toggles.forEach(function($el) {
+ $el.toggleClass('active');
+ })
+ }
+ });
+
+ $(".resource-category").on("submit", "form", function(e) {
+ var $parent = $(e.delegateTarget);
+ $parent.find(".resource-input").each(function(index) {
+ var $input = $(this);
+ var parsed = JSON.parse($input.val());
+ parsed.__index = index;
+ $input.val(JSON.stringify(parsed));
+ })
+ });
function pressEnter(fn){
return function(e){
diff --git a/themes/okadmin/templates/index.liquid b/themes/okadmin/templates/index.liquid
index 95c64dd..0672613 100644
--- a/themes/okadmin/templates/index.liquid
+++ b/themes/okadmin/templates/index.liquid
@@ -1,23 +1,36 @@
{% include 'partials/head' %}
+{% include 'partials/flash' %}
+
<section class="index main">
{% for pair in resources %}
{% assign name = pair[0] %}
{% assign resource = pair[1] %}
- {% assign spec = resource.spec %}
<section class="resource-category {{name}}">
- <header>
- <h2>{{name | capitalize}}</h2>
- </header>
- <ul class="resource-list">
- {% for data in resource.data %}
- <li><a href="{{resource.type}}/{{data.id}}/">{{data.id}}</a></li>
- {% endfor %}
- </ul>
- <footer>
- <a class="add-new" href="{{resource.type}}/new/">+</a>
- </footer>
+ <form action="{{resource.type}}/__batch/" method="POST">
+ <header>
+ <h2>{{name | capitalize}}</h2>
+ </header>
+ <input type="hidden" name="_method" value="PUT">
+ <ol class="resource-list">
+ {% for data in resource.data %}
+ <li>
+ <a href="{{resource.type}}/{{data.id}}/">{{data.id}}</a>
+ <input class="resource-input" type="hidden" name="{{resource.type}}[{{forloop.index0}}]" value='{{data | stringify}}'>
+ </li>
+ {% endfor %}
+ </ol>
+ <footer>
+ <nav>
+ <a class="btn cancel-btn" href="#">cancel</a>
+ <button type="submit"
+ class="btn save-btn" href="#">save</button>
+ <a class="btn edit-btn active" href="#">edit</a>
+ <a class="btn add-btn active" href="{{resource.type}}/new/">+</a>
+ </nav>
+ </footer>
+ </form>
</section>
{% endfor %}
diff --git a/themes/okadmin/templates/partials/errors.liquid b/themes/okadmin/templates/partials/flash.liquid
index cdb0b25..1980ab5 100644
--- a/themes/okadmin/templates/partials/errors.liquid
+++ b/themes/okadmin/templates/partials/flash.liquid
@@ -1,10 +1,13 @@
+<div class="success">
+ {% for info in success %}
+ <div class="message">{{info.action}}</div>
+ {% endfor %}
+</div>
<div class="errors">
{% for error in errors %}
<div class="error">
<div class="message">{{error.message}}</div>
- <div class="assertion">
- Expected {{error.expected}} but got {{error.actual}}
- </div>
</div>
{% endfor %}
</div>
+
diff --git a/themes/okadmin/templates/partials/inputs.liquid b/themes/okadmin/templates/partials/inputs.liquid
index 7d23c9e..99258f3 100644
--- a/themes/okadmin/templates/partials/inputs.liquid
+++ b/themes/okadmin/templates/partials/inputs.liquid
@@ -4,24 +4,28 @@
{% assign type = spec.type %}
<div class="property {{type}}">
- <label for="{{name}}">{{name | capitalize}}</label>
+ <label
+ {% if spec.hidden %}
+ class="hidden"
+ {% endif %}
+ for="{{name}}">{{name | capitalize}}</label>
{% if type == 'string' %}
<input
- {% if spec.disabled %}
- disabled="true"
+ {% if spec.hidden %}
+ class="hidden"
{% endif %}
name="{{name}}" type="text" value="{{spec.value}}">
{% elsif type == 'text' %}
<textarea
- {% if spec.disabled %}
- disabled="true"
+ {% if spec.hidden %}
+ class="hidden"
{% endif %}
name="{{name}}">{{spec.value}}</textarea>
{% elsif type == 'enum' %}
<select
- {% if spec.disabled %}
- disabled="true"
+ {% if spec.hidden %}
+ class="hidden"
{% endif %}
name="{{name}}">
{% for option in spec.options %}
@@ -43,8 +47,8 @@
<ol>
{% for image in spec.value %}
<li class="image-element">
- <input type="hidden" name="{{name}}[][uri]" value="{{image.uri}}">
- <textarea class="caption" name="{{name}}[][caption]">{{image.caption}}</textarea>
+ <input type="hidden" name="{{name}}[{{forloop.index0}}][uri]" value="{{image.uri}}">
+ <textarea class="caption" name="{{name}}[{{forloop.index0}}][caption]">{{image.caption}}</textarea>
<img src="{{image.uri}}" alt="{{image.caption}}">
<button class="remove-image">♲</button>
</li>
@@ -64,6 +68,8 @@
</li>
</script>
</div>
+ {% elsif type == 'meta' %}
+ <input class="hidden" type="hidden" name="{{name}}" value="{{spec.value}}">
{% else %}
<p><pre style="color: red">Admin template doesn't support '{{type}}' properties!</pre></p>
{% endif %}
diff --git a/themes/okadmin/templates/resource.liquid b/themes/okadmin/templates/resource.liquid
index c321e8a..8078778 100644
--- a/themes/okadmin/templates/resource.liquid
+++ b/themes/okadmin/templates/resource.liquid
@@ -1,8 +1,8 @@
{% include 'partials/head' %}
-{% include 'partials/errors' %}
+{% include 'partials/flash' %}
-<nav>
+<nav class="resource-nav">
<a href="../..">Back</a>
</nav>
diff --git a/themes/okadmin/templates/resource_new.liquid b/themes/okadmin/templates/resource_new.liquid
index c57dd83..d059445 100644
--- a/themes/okadmin/templates/resource_new.liquid
+++ b/themes/okadmin/templates/resource_new.liquid
@@ -1,8 +1,8 @@
{% include 'partials/head' %}
-{% include 'partials/errors' %}
+{% include 'partials/flash' %}
-<nav>
+<nav class="resource-nav">
<a href="../..">Back</a>
</nav>