From b91c88be52170a7c9a8b133ef72052bec8caccba Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Wed, 8 Apr 2015 02:30:15 -0400 Subject: Wrapping liquid lib's bluebird promises with Q promises Can consider porting the whole thing to bluebird promises later --- app/node_modules/oktemplate/index.js | 7 ++++++- app/node_modules/oktemplate/package.json | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) (limited to 'app/node_modules') diff --git a/app/node_modules/oktemplate/index.js b/app/node_modules/oktemplate/index.js index 700020c..dafe5e6 100644 --- a/app/node_modules/oktemplate/index.js +++ b/app/node_modules/oktemplate/index.js @@ -1,3 +1,4 @@ +var Q = require('q'); var fs = require('fs'); var path = require('path'); var glob = require('glob'); @@ -56,8 +57,12 @@ OKTemplateRepo.prototype._populateCache = function _populateCache(engine, cache, name: name, templateString: templateString, render: function(data) { + return Q.promise(function(resolve, reject) { // TODO Not sure if this caches parsed templates behind the scenes? - return engine.parseAndRender(templateString, data); + engine.parseAndRender(templateString, data) + .then(resolve) + .catch(reject); + }); } } }); diff --git a/app/node_modules/oktemplate/package.json b/app/node_modules/oktemplate/package.json index 3e92ccb..70e94e3 100644 --- a/app/node_modules/oktemplate/package.json +++ b/app/node_modules/oktemplate/package.json @@ -12,6 +12,7 @@ "bluebird": "^2.9.21", "glob": "^5.0.3", "json-to-html": "^0.1.2", - "liquid-node": "^2.5.0" + "liquid-node": "^2.5.0", + "q": "^1.2.0" } } -- cgit v1.2.3-70-g09d2 From 0fc380788157779a39fe630b2464280eea9f5088 Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Wed, 8 Apr 2015 14:06:30 -0400 Subject: Add resource instance concept Can now define resource classes bound to particular data points, in particular they can be static data defined by app config and not even in DB --- app/index.js | 56 +++++++--- app/node_modules/okquery/index.js | 26 +++-- app/node_modules/okresource/index.js | 178 ++++++++++++++++++++++++++++--- app/node_modules/okresource/package.json | 1 + 4 files changed, 230 insertions(+), 31 deletions(-) (limited to 'app/node_modules') diff --git a/app/index.js b/app/index.js index c8dceb5..ca09079 100644 --- a/app/index.js +++ b/app/index.js @@ -53,12 +53,13 @@ function OKCMS(options) { } }; var schemas = this._schemas = this._createSchemas(schemaConfig); - var resources = this._resources = + var resourceCache = this._resourceCache = this._createResources(resourceConfig, db, schemas); // Create view instances from config var views = this._views = - this._createViews(viewConfig, db, meta, resources, templateProvider); + this._createViews(viewConfig, db, meta, resourceCache, templateProvider); + var server = this._server = new OKServer({ express: express, app: app, @@ -85,22 +86,24 @@ OKCMS.prototype._createSchemas = function(schemaConfig) { OKCMS.prototype._createResources = function(resourceConfig, db, schemaCache) { resourceConfig = resourceConfig || {}; - var typeCache = {}; - return resourceConfig.reduce(function(cache, config) { + var resources = resourceConfig.map(function(config) { var type = config.type; var schema = schemaCache[type]; if (!schema) throw new Error('Resource config references nonexistent schema'); - // If we already created resource class, just skip - if (cache[type]) - return cache; - cache[type] = OKResource({ + var resource = OKResource({ type: type, db: db, schema: schema - }) - return cache; - }, {}); + }); + // Static resources have some data defined by configuration and + // are a special case + if (config.static) { + resource = resource.instance({static: config.static}); + } + return resource; + }); + return ResourceCache(resources); }; OKCMS.prototype._createViews = function(viewConfig, db, @@ -150,7 +153,7 @@ OKCMS.prototype._createQueries = function(queryConfig, resourceCache) { queryConfig = [queryConfig]; return queryConfig.map(function(config) { var type = config.type; - var resource = resourceCache[type]; + var resource = resourceCache.get(type); if (!resource) throw new Error('Query configured with nonexistent resource'); // Default to "select all" query @@ -162,6 +165,35 @@ OKCMS.prototype._createQueries = function(queryConfig, resourceCache) { }); }; +/** + * Stupid lil cache to help deal with the fact that + * resources can be indexed by either type or a type + id combo. + */ +function ResourceCache(resources) { + if (!(this instanceof ResourceCache)) return new ResourceCache(resources); + resources = resources || []; + var cache = this._cache = {}; + resources.forEach(function(resource) { + if (!resource) + throw new Error('Undefined resource given to ResourceCache'); + if (resource.bound) { + cache[resource.type] = resource.parent; + cache[resource.type + ':' + resource.id] = resource; + } else { + cache[resource.type] = resource; + } + }); +} + +ResourceCache.prototype.get = function(type, id) { + if (!type) return; + if (id && this._cache[type + ':' + id]) { + return this._cache[type + ':' + id]; + } else { + return this._cache[type]; + } +}; + module.exports = { createApp: function(options) { diff --git a/app/node_modules/okquery/index.js b/app/node_modules/okquery/index.js index a64f0f2..9748067 100644 --- a/app/node_modules/okquery/index.js +++ b/app/node_modules/okquery/index.js @@ -14,14 +14,19 @@ function OKQuery(options) { var resource = options.resource; var type = resource.type; var query = options.query || '*'; + Object.defineProperty(this, 'resource', { value: resource, - writable: false + writable: false, + enumerable: true }); + Object.defineProperty(this, 'type', { value: resource.type, - writable: false + writable: false, + enumerable: true }); + this.get = createQuery(resource, query, { default: options.default }); @@ -29,8 +34,9 @@ function OKQuery(options) { function createQuery(resource, query, options) { options = options || {}; - var query; - if (isDynamic(query)) { + if (resource.bound) { + query = queryBound(resource); + } else if (isDynamic(query)) { query = queryDynamic(resource); } else if (isSet(query)) { query = queryAll(resource); @@ -46,19 +52,25 @@ function createQuery(resource, query, options) { function queryDynamic(resource) { return function(id) { return resource.get(id); - } + }; } function queryAll(resource) { return function() { return resource.all(); - } + }; } function querySingle(resource, id) { return function() { return resource.get(id); - } + }; +} + +function queryBound(resource) { + return function() { + return resource.get(); + }; } function withDefault(queryFn, resultDefault) { diff --git a/app/node_modules/okresource/index.js b/app/node_modules/okresource/index.js index 7cd51c7..0e99883 100644 --- a/app/node_modules/okresource/index.js +++ b/app/node_modules/okresource/index.js @@ -1,3 +1,4 @@ +var assign = require('object-assign'); var Q = require('q'); /** @@ -31,15 +32,10 @@ function OKResource(options) { var type = options.type; this._db = options.db; + this._schema = schema; // Define properties which are part of the API - Object.defineProperty(this, 'schema', { - value: schema, - writable: false, - enumerable: true - }); - Object.defineProperty(this, 'spec', { value: schema.spec, writable: false, @@ -57,13 +53,21 @@ function OKResource(options) { writable: false, enumerable: true }); + + // Whether this resource represents a specific data point + // or a whole class of data + Object.defineProperty(this, 'bound', { + value: false, + writable: false, + enumerable: true + }); } /** * Throws an error if data does not conform to schema */ OKResource.prototype.assertValid = function(data) { - this.schema.assertValid(data); + this._schema.assertValid(data); }; OKResource.prototype.all = function() { @@ -72,12 +76,14 @@ OKResource.prototype.all = function() { OKResource.prototype.create = function(data) { data = 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')); } else { - this._db.create(this.type, data).then(resolve, reject); + db.create(type, data).then(resolve, reject); } }); }; @@ -145,14 +151,14 @@ OKResource.prototype.updateOrCreate = function(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('Cannot updateOrCreate without ID')); } else { - db.get(type, data.id).then(function(cached) { - var query = {}; - query[idField] = id; - if (cached) + db.get(type, query).then(function(persisted) { + if (persisted) db.put(type, query, data).then(resolve, reject); else db.create(type, data).then(resolve, reject); @@ -161,4 +167,152 @@ OKResource.prototype.updateOrCreate = function(data) { }); }; +/** + * Create special resource which is bound to particular data point + * and has custom validation and query properties. + */ +OKResource.prototype.instance = function(options) { + return new OKResourceInstance(this, { + static: options.static + }); +}; + +function OKResourceInstance(resource, options) { + if (!(this instanceof OKResourceInstance)) return new OKResourceInstance(options); + // Only support static data instances for now + if (!options.static) + throw new Error( + 'Cannot create OKResourceInstance without static data'); + + // Resources with static data are a special case. They exist + // conceptually at all times since they are derived from app + // configuration, but may not actually be present + // in the database and need custom logic to handle this. + var staticData = assign({}, options.static); + var id = staticData[resource.idField]; + if (!id) + throw new Error( + 'Cannot create static OKResourceInstance without an ID field'); + + /** + * Ensure that static data is provided on get + */ + this.get = function() { + return Q.promise(function(resolve, reject) { + resource.get(id).then(function(data) { + // Note the assign call. Don't expose private references! + if (data) { + resolve(assign({}, data, staticData)); + } else { + resolve(assign({}, staticData)); + } + }).fail(reject); + }); + }; + + this.update = function(data) { + return Q.promise(function(resolve, reject) { + var valid = Object.keys(staticData).every(function(prop) { + return staticData[prop] === data[prop]; + }); + if (!valid) { + reject(new Error('Cannot update resource\'s static data')); + } else { + // When updating static resources, we create them if + // they don't actually exist in the DB + resource.updateOrCreate(data).then(resolve).fail(reject); + } + }); + }; + + this.updateOrCreate = function(id) { + return Q.promise(function(resolve, reject) { + reject(new Error('Cannot updateOrCreate static resource')); + }); + }; + + this.destroy = function(id) { + return Q.promise(function(resolve, reject) { + reject(new Error('Cannot destroy static resource')); + }); + }; + + this.all = function(id) { + return Q.promise(function(resolve, reject) { + reject(new Error('Cannot get all for static resource')); + }); + }; + + this.create = function(id) { + return Q.promise(function(resolve, reject) { + reject(new Error('Cannot create static resource')); + }); + }; + + this.find = function(id) { + return Q.promise(function(resolve, reject) { + reject(new Error('Cannot perform find on static resource')); + }); + }; + + this.assertValid = function(data) { + data = data || {}; + Object.keys(staticData).forEach(function(prop) { + if (staticData[prop] !== data[prop]) { + // Validation error is in mschema error format + throw [{ + property: prop, + constraint: 'static', + expected: staticData[prop], + actual: data[prop], + message: 'Data does not match static data' + }]; + } + }); + resource.assertValid(data); + }; + + Object.defineProperty(this, 'parent', { + value: resource, + writable: false, + enumerable: true + }); + + Object.defineProperty(this, 'spec', { + value: resource.spec, + writable: false, + enumerable: true + }); + + Object.defineProperty(this, 'id', { + value: id, + writable: false, + enumerable: true + }); + + Object.defineProperty(this, 'type', { + value: resource.type, + writable: false, + 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/okresource/package.json b/app/node_modules/okresource/package.json index da9dfe0..7f19c9b 100644 --- a/app/node_modules/okresource/package.json +++ b/app/node_modules/okresource/package.json @@ -9,6 +9,7 @@ "author": "OKFocus", "license": "None", "dependencies": { + "object-assign": "^2.0.0", "q": "^1.2.0" } } -- cgit v1.2.3-70-g09d2 From f768fc2d247988bafc80f43bda3a4a1d89a31adb Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Wed, 8 Apr 2015 14:17:08 -0400 Subject: Don't just reject, fail! Changed Q promise stuff in OKResource so that it properly catches exceptions in promises. --- app/node_modules/okresource/index.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) (limited to 'app/node_modules') diff --git a/app/node_modules/okresource/index.js b/app/node_modules/okresource/index.js index 0e99883..122b293 100644 --- a/app/node_modules/okresource/index.js +++ b/app/node_modules/okresource/index.js @@ -83,7 +83,7 @@ OKResource.prototype.create = function(data) { if (!id) { reject(new Error('Data does not contain ID property')); } else { - db.create(type, data).then(resolve, reject); + db.create(type, data).then(resolve).fail(reject); } }); }; @@ -95,7 +95,7 @@ OKResource.prototype.destroy = function(data) { if (!id) { reject(new Error('Data does not contain ID property')); } else { - this._db.remove(this.type, data.id, data).then(resolve, reject); + this._db.remove(this.type, data.id, data).then(resolve).fail(reject); } }); }; @@ -105,7 +105,7 @@ OKResource.prototype.find = function(query) { if (!query) { throw new Error('No query given'); } else { - this._db.find(this.type, query).then(resolve, reject); + this._db.find(this.type, query).then(resolve).fail(reject); } }); }; @@ -123,7 +123,7 @@ OKResource.prototype.get = function(id) { // to match var query = {}; query[idField] = id; - db.get(type, query).then(resolve, reject); + db.get(type, query).then(resolve).fail(reject); } }); }; @@ -140,7 +140,7 @@ OKResource.prototype.update = function(data) { } else { var query = {}; query[idField] = data[idField]; - db.put(type, query, data).then(resolve, reject);; + db.put(type, query, data).then(resolve).fail(reject);; } }); }; @@ -158,11 +158,12 @@ OKResource.prototype.updateOrCreate = function(data) { reject(new Error('Cannot updateOrCreate without ID')); } else { db.get(type, query).then(function(persisted) { - if (persisted) - db.put(type, query, data).then(resolve, reject); - else - db.create(type, data).then(resolve, reject); - }, reject); + if (persisted) { + db.put(type, query, data).then(resolve).fail(reject); + } else { + db.create(type, data).then(resolve).fail(reject); + } + }).fail(reject); } }); }; -- cgit v1.2.3-70-g09d2 From 88d83bc72eb7162dc8877de1aaf5269ee10e41bc Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Wed, 8 Apr 2015 14:27:30 -0400 Subject: Implement FSDB.create --- app/node_modules/okdb/index.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'app/node_modules') diff --git a/app/node_modules/okdb/index.js b/app/node_modules/okdb/index.js index 3089411..5368e4a 100644 --- a/app/node_modules/okdb/index.js +++ b/app/node_modules/okdb/index.js @@ -72,8 +72,13 @@ FSDB.prototype.put = function(collection, query, data) { } }; -FSDB.prototype.create = function(collection, id, data) { - throw new Error('Not implemented!'); +FSDB.prototype.create = function(collection, data) { + var created = this._db(collection) + .chain() + .push(data) + .last() + .value(); + return this._resolve(created); }; FSDB.prototype.remove = function(collection, id, data) { -- cgit v1.2.3-70-g09d2 From 2a4ede6507f55e492736c4b82a139ac5d2e2eec0 Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Wed, 8 Apr 2015 15:04:48 -0400 Subject: Update OKAdminView template data fetch for custom IDs --- app/node_modules/okutil/index.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) (limited to 'app/node_modules') diff --git a/app/node_modules/okutil/index.js b/app/node_modules/okutil/index.js index 738c6a4..01041b6 100644 --- a/app/node_modules/okutil/index.js +++ b/app/node_modules/okutil/index.js @@ -24,17 +24,27 @@ module.exports = { }))) .then(function(results) { var metadata = results.shift(); - var normalized = results.reduce(function(data, result, i) { + var normalized = results.reduce(function(cache, result, i) { + var resource = queries[i].resource; var type = queries[i].type; + var plural = pluralize(type); if (isarray(result)) { - data[pluralize(type)] = result; + result = result.map(function(data) { + // Inform template of ID in generic way + data.id = data[resource.idField]; + return data; + }); } else { - data[type] = result; + // Inform template of ID in generic way + result.id = result[resource.idField]; + result = [result] } - return data; + cache[plural] = cache[plural] || []; + cache[plural] = cache[plural].concat(result); + return cache; }, {meta: metadata}); resolve(normalized); - }, reject); + }).fail(reject); }); }, -- cgit v1.2.3-70-g09d2 From 5d676437f64791b435d7554d9ec4f4628d0abcc3 Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Wed, 8 Apr 2015 15:07:50 -0400 Subject: Remove unused REST endpoint module --- app/index.js | 5 +---- app/node_modules/okrest/index.js | 20 -------------------- app/node_modules/okrest/package.json | 11 ----------- 3 files changed, 1 insertion(+), 35 deletions(-) delete mode 100644 app/node_modules/okrest/index.js delete mode 100644 app/node_modules/okrest/package.json (limited to 'app/node_modules') diff --git a/app/index.js b/app/index.js index ca09079..0e83363 100644 --- a/app/index.js +++ b/app/index.js @@ -10,7 +10,6 @@ var OKDB = require('okdb'); var OKResource = require('okresource') var OKTemplate = require('oktemplate'); var OKServer = require('okserver'); -var OKRestEndpoint = require('okrest'); var OKSchema = require('okschema'); /** @@ -202,8 +201,6 @@ module.exports = { OKResource: OKResource, - OKView: OKView, - - OKRestEndpoint: OKRestEndpoint + OKView: OKView }; diff --git a/app/node_modules/okrest/index.js b/app/node_modules/okrest/index.js deleted file mode 100644 index 169626d..0000000 --- a/app/node_modules/okrest/index.js +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 462c890..0000000 --- a/app/node_modules/okrest/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "okrest", - "version": "1.0.0", - "description": "nice", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "OKFocus", - "license": "None" -} -- cgit v1.2.3-70-g09d2 From 565feb87740dcc693be50f8dbde4887e4ebdc79f Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Wed, 8 Apr 2015 16:05:56 -0400 Subject: Move fetchTemplateData back into OKAdminView --- app/node_modules/okutil/index.js | 39 ------------------------- app/node_modules/okview/index.js | 56 +++++++++++++++++++++++++++++++++++- app/node_modules/okview/package.json | 7 ++++- 3 files changed, 61 insertions(+), 41 deletions(-) (limited to 'app/node_modules') diff --git a/app/node_modules/okutil/index.js b/app/node_modules/okutil/index.js index 01041b6..3142ae1 100644 --- a/app/node_modules/okutil/index.js +++ b/app/node_modules/okutil/index.js @@ -9,45 +9,6 @@ var Q = require('q'); */ module.exports = { - /** - * Takes a meta data query and an array of resource queries - * and returns a promise for an object merging all queried - * data, pluralizing keys where necessary. - * - * Lil bit convoluted, sorry. - */ - fetchTemplateData: function fetchTemplateData(meta, queries, options) { - return Q.promise(function(resolve, reject) { - return Q.all( - [meta.get()].concat(queries.map(function(query) { - return query.get(options); - }))) - .then(function(results) { - var metadata = results.shift(); - var normalized = results.reduce(function(cache, result, i) { - var resource = queries[i].resource; - var type = queries[i].type; - var plural = pluralize(type); - if (isarray(result)) { - result = result.map(function(data) { - // Inform template of ID in generic way - data.id = data[resource.idField]; - return data; - }); - } else { - // Inform template of ID in generic way - result.id = result[resource.idField]; - result = [result] - } - cache[plural] = cache[plural] || []; - cache[plural] = cache[plural].concat(result); - return cache; - }, {meta: metadata}); - resolve(normalized); - }).fail(reject); - }); - }, - /** * Return a copy of the route with a trailing slash */ diff --git a/app/node_modules/okview/index.js b/app/node_modules/okview/index.js index 1ceac03..5f3a8c9 100644 --- a/app/node_modules/okview/index.js +++ b/app/node_modules/okview/index.js @@ -1,4 +1,7 @@ -var fetchTemplateData = require('okutil').fetchTemplateData; +var assign = require('object-assign'); +var pluralize = require('pluralize'); +var isarray = require('lodash.isarray'); +var Q = require('q'); var OKResource = require('okresource'); // Routes for views over collections have a special pattern @@ -129,4 +132,55 @@ function getParamName(route) { return matches[1]; } +/** + * Takes a meta data query and an array of resource queries + * and returns a promise for an object merging all queried + * data, pluralizing keys where necessary. + * + * Lil bit convoluted, sorry. + */ +function fetchTemplateData(meta, queries) { + return Q.promise(function(resolve, reject) { + return Q.all( + [meta.get()].concat(queries.map(function(query) { + return query.get(); + }))) + .then(function(results) { + var metadata = results.shift(); + var normalized = results.reduce(function(cache, result, i) { + // Huh? Bail + if (!result) + return cache; + var resource = queries[i].resource; + var type = queries[i].type; + var manyResult = isarray(result); + // Inform template of ID in generic field + if (manyResult) { + result = result.map(function(data) { + return assign({}, data, {id: data[resource.idField]}) + }); + } else { + result = assign({}, result, {id: result[resource.idField]}); + } + // If we have a lot of results for a certain type, + // we pluralize the key and yield an array of results + if (cache[type] || manyResult) { + var plural = pluralize(type); + delete cache[type]; + cache[plural] = []; + if (manyResult) { + cache[plural] = cache[plural].concat(result); + } else { + cache[plural].push(result); + } + } else { + cache[type] = result; + } + return cache; + }, {meta: metadata}); + resolve(normalized); + }).fail(reject); + }); +} + module.exports = OKView; diff --git a/app/node_modules/okview/package.json b/app/node_modules/okview/package.json index 8d95b40..bbf4e40 100644 --- a/app/node_modules/okview/package.json +++ b/app/node_modules/okview/package.json @@ -8,5 +8,10 @@ }, "author": "OKFocus", "license": "None", - "dependencies": {} + "dependencies": { + "lodash.isarray": "^3.0.1", + "object-assign": "^2.0.0", + "pluralize": "^1.1.2", + "q": "^1.2.0" + } } -- cgit v1.2.3-70-g09d2 From 29b81b08d925596694ef5dc1b80a9fff6e4625f7 Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Wed, 8 Apr 2015 16:11:56 -0400 Subject: Fix up OKView a wee bit --- app/node_modules/okview/index.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) (limited to 'app/node_modules') diff --git a/app/node_modules/okview/index.js b/app/node_modules/okview/index.js index 5f3a8c9..e9c0bfe 100644 --- a/app/node_modules/okview/index.js +++ b/app/node_modules/okview/index.js @@ -2,6 +2,7 @@ var assign = require('object-assign'); var pluralize = require('pluralize'); var isarray = require('lodash.isarray'); var Q = require('q'); +var OKQuery = require('okquery'); var OKResource = require('okresource'); // Routes for views over collections have a special pattern @@ -18,11 +19,11 @@ function OKView(options) { if (!(this instanceof OKView)) return new OKView(options); options = options || {}; if (!options.template) - throw new Error('No template provided to view.'); + throw new Error('No template provided to OKView.'); if (!options.meta) - throw new Error('No meta resource provided to view'); + throw new Error('No meta resource provided to OKView'); if (!options.route) - throw new Error('No route provided to view'); + throw new Error('No route provided to OKView'); var route = options.route; var mount = options.mount || 'get'; this._template = options.template; @@ -32,28 +33,29 @@ function OKView(options) { // resource will be resolved later // TODO This bound / unbound thing can probably be expressed in a // less convoluted way. - var unbound = this.unbound = !!UNBOUND_ROUTE_PATTERN.exec(this.route); + var unbound = this.unbound = !!UNBOUND_ROUTE_PATTERN.exec(route); + Object.defineProperty(this, 'mount', { value: mount, writable: false, enumerable: true }); + Object.defineProperty(this, 'route', { value: route, writable: false, enumerable: true }); + this._middleware = createMiddleware(this); - this._fetchTemplateData = unbound ? - fetchResourceTemplateData : fetchCollectionTemplateData; + this._fetchTemplateData = unbound ? fetchUnbound : fetchBound; - function fetchResourceTemplateData(id) { - // Bound views only have a single query - // TODO This is super convoluted + function fetchUnbound(id) { + // TODO Janky return fetchTemplateData(meta, [queries[0].get(id)]); } - function fetchCollectionTemplateData() { + function fetchBound() { return fetchTemplateData(meta, queries); } } @@ -65,7 +67,7 @@ OKView.prototype.middleware = function() { OKView.prototype.render = function(req, res, data) { this._template.render(data).then(function(html) { res.send(html); - }, errorHandler(req, res, data)); + }).fail(errorHandler(req, res, data)); }; OKView.prototype.fetchTemplateData = function() { @@ -97,7 +99,7 @@ function unboundMiddleware(view) { var id = req.params[paramName]; view.fetchTemplateData(id).then(function(data) { view.render(req, res, data); - }, errorHandler(req, res, next)); + }).fail(errorHandler(req, res, next)); }; } @@ -109,7 +111,7 @@ function boundMiddleware(view) { return function(req, res, next) { view.fetchTemplateData().then(function(data) { view.render(req, res, data); - }, errorHandler(req, res, next)); + }).fail(errorHandler(req, res, next)); }; } -- cgit v1.2.3-70-g09d2 From f7a6d15e313bf5502a885a6736c7f0de161a9520 Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Wed, 8 Apr 2015 18:10:48 -0400 Subject: Intercept and 404 favicon requests for now --- app/node_modules/okserver/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'app/node_modules') diff --git a/app/node_modules/okserver/index.js b/app/node_modules/okserver/index.js index 952602d..e6145ee 100644 --- a/app/node_modules/okserver/index.js +++ b/app/node_modules/okserver/index.js @@ -45,9 +45,14 @@ function OKServer(options) { * Order is important here! Requests go down the middleware * chain until they are handled with a response, which could * happen anywhere in the chain. Watch out for middleware shadowing - * ither middleware. + * other middleware. */ + // Intercept favicon requests and 404 for now + app.use('/favicon.ico', function(req, res) { + res.status(404) + return res.send(''); + }); // Serve user static files app.use(express.static(root)); // Serve admin interface static files -- cgit v1.2.3-70-g09d2 From e2e9e4bb6a9e90687292961526a82ac881f2f3bb Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Wed, 8 Apr 2015 18:25:10 -0400 Subject: Fix resource bug where default id not set correctly --- app/node_modules/okresource/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/node_modules') diff --git a/app/node_modules/okresource/index.js b/app/node_modules/okresource/index.js index 122b293..6010d7d 100644 --- a/app/node_modules/okresource/index.js +++ b/app/node_modules/okresource/index.js @@ -26,7 +26,7 @@ function OKResource(options) { idField = prop; return idField; // If schema has a prop called 'id', default to that one - }, schema.id && 'id'); + }, schema.spec.id && 'id'); if (!idField) throw new Error('Bad schema: no ID field'); -- cgit v1.2.3-70-g09d2 From bb3d8da23279dc2e4cf275b08b03148b3980fb01 Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Wed, 8 Apr 2015 18:28:26 -0400 Subject: Fix bug where views didn't resolve resource correctly --- app/index.js | 2 +- app/node_modules/okview/index.js | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) (limited to 'app/node_modules') diff --git a/app/index.js b/app/index.js index 0e83363..792c10a 100644 --- a/app/index.js +++ b/app/index.js @@ -152,7 +152,7 @@ OKCMS.prototype._createQueries = function(queryConfig, resourceCache) { queryConfig = [queryConfig]; return queryConfig.map(function(config) { var type = config.type; - var resource = resourceCache.get(type); + var resource = resourceCache.get(type, config.query); if (!resource) throw new Error('Query configured with nonexistent resource'); // Default to "select all" query diff --git a/app/node_modules/okview/index.js b/app/node_modules/okview/index.js index e9c0bfe..c245b0c 100644 --- a/app/node_modules/okview/index.js +++ b/app/node_modules/okview/index.js @@ -51,8 +51,11 @@ function OKView(options) { this._fetchTemplateData = unbound ? fetchUnbound : fetchBound; function fetchUnbound(id) { - // TODO Janky - return fetchTemplateData(meta, [queries[0].get(id)]); + var resource = queries[0].resource; + return fetchTemplateData(meta, [OKQuery({ + resource: resource, + query: id + })]); } function fetchBound() { @@ -150,9 +153,10 @@ function fetchTemplateData(meta, queries) { .then(function(results) { var metadata = results.shift(); var normalized = results.reduce(function(cache, result, i) { - // Huh? Bail - if (!result) + // Could be just some rogue request + if (!result) { return cache; + } var resource = queries[i].resource; var type = queries[i].type; var manyResult = isarray(result); -- cgit v1.2.3-70-g09d2 From 5f3d47c4bea9c94152a8243c304ee0f43e84ac07 Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Wed, 8 Apr 2015 19:50:19 -0400 Subject: Proper API for resource updates --- app/node_modules/okresource/index.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) (limited to 'app/node_modules') diff --git a/app/node_modules/okresource/index.js b/app/node_modules/okresource/index.js index 6010d7d..80279dc 100644 --- a/app/node_modules/okresource/index.js +++ b/app/node_modules/okresource/index.js @@ -128,26 +128,24 @@ OKResource.prototype.get = function(id) { }); }; -OKResource.prototype.update = function(data) { +OKResource.prototype.update = function(id, data) { data = data || {}; - var id = data[this.idField]; var db = this._db; var type = this.type; var idField = this.idField; return Q.promise(function(resolve, reject) { if (!id) { - reject(new Error('Data does not contain ID property')); + reject(new Error('No resource ID provided')); } else { var query = {}; - query[idField] = data[idField]; + query[idField] = id; db.put(type, query, data).then(resolve).fail(reject);; } }); }; -OKResource.prototype.updateOrCreate = function(data) { +OKResource.prototype.updateOrCreate = function(id, data) { data = data || {}; - var id = data[this.idField]; var type = this.type; var db = this._db; var idField = this.idField; @@ -155,7 +153,7 @@ OKResource.prototype.updateOrCreate = function(data) { query[idField] = id; return Q.promise(function(resolve, reject) { if (!id) { - reject(new Error('Cannot updateOrCreate without ID')); + reject(new Error('No resource ID provided')); } else { db.get(type, query).then(function(persisted) { if (persisted) { @@ -221,12 +219,12 @@ function OKResourceInstance(resource, options) { } else { // When updating static resources, we create them if // they don't actually exist in the DB - resource.updateOrCreate(data).then(resolve).fail(reject); + resource.updateOrCreate(id, data).then(resolve).fail(reject); } }); }; - this.updateOrCreate = function(id) { + this.updateOrCreate = function(data) { return Q.promise(function(resolve, reject) { reject(new Error('Cannot updateOrCreate static resource')); }); -- cgit v1.2.3-70-g09d2 From 32f87fcb58466e0e311349f96751c18e2a2cd7ea Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Wed, 8 Apr 2015 19:55:34 -0400 Subject: Administrate! --- app/index.js | 42 +++- app/node_modules/okadminview/index.js | 320 ++++++++++++++++++++++++ app/node_modules/okadminview/package.json | 17 ++ package.json | 1 + themes/okadmin/public/css/main.css | 103 ++++++++ themes/okadmin/templates/index.liquid | 26 ++ themes/okadmin/templates/partials/head.liquid | 13 + themes/okadmin/templates/partials/inputs.liquid | 19 ++ themes/okadmin/templates/partials/tail.liquid | 3 + themes/okadmin/templates/resource.liquid | 15 ++ themes/okadmin/templates/resource_new.liquid | 15 ++ 11 files changed, 571 insertions(+), 3 deletions(-) create mode 100644 app/node_modules/okadminview/index.js create mode 100644 app/node_modules/okadminview/package.json create mode 100644 themes/okadmin/public/css/main.css create mode 100644 themes/okadmin/templates/index.liquid create mode 100644 themes/okadmin/templates/partials/head.liquid create mode 100644 themes/okadmin/templates/partials/inputs.liquid create mode 100644 themes/okadmin/templates/partials/tail.liquid create mode 100644 themes/okadmin/templates/resource.liquid create mode 100644 themes/okadmin/templates/resource_new.liquid (limited to 'app/node_modules') diff --git a/app/index.js b/app/index.js index 94d87dc..4ec8730 100644 --- a/app/index.js +++ b/app/index.js @@ -7,6 +7,7 @@ var express = require('express'); var Q = require('q'); var OKQuery = require('okquery'); var OKView = require('okview'); +var OKAdminView = require('okadminview'); var OKDB = require('okdb'); var OKResource = require('okresource') var OKTemplate = require('oktemplate'); @@ -80,11 +81,16 @@ function OKCMS(options) { // Create view instances from config var views = this._views = this._createViews(viewConfig, db, meta, resourceCache, templateProvider); + var adminViews = this._adminViews = + this._createAdminViews(adminPath, app, express, resourceConfig, + resourceCache, adminTemplateProvider, adminMeta); var server = this._server = new OKServer({ express: express, app: app, - views: views, + // Merge admin views with normal views + views: assign(views, adminViews), + // Specify root folders and paths for serving static assets root: root, adminRoot: adminRoot, adminPath: adminPath @@ -166,7 +172,37 @@ OKCMS.prototype._createViews = function(viewConfig, db, else return '404'; } -} +}; + +OKCMS.prototype._createAdminViews = function(path, app, express, + resourceConfig, resourceCache, templateProvider, meta) { + var views = {}; + var withTrail = withTrailingSlash(path); + var withoutTrail = withoutTrailingSlash(path); + // Stoopid fix for a bug in Express. Need to do this + // to ensure strict routing is not broken for the nested + // admin router. + // See: https://github.com/strongloop/express/issues/2281 + // TODO Get rid of this crap + views[withoutTrail] = { + mount: 'get', + middleware: function() { + return function(req, res) { + res.redirect(301, withTrail); + } + } + }; + // Add real view at trailing slash route + views[withTrail] = OKAdminView({ + app: app, + express: express, + resourceConfig: resourceConfig, + resourceCache: resourceCache, + templateProvider: templateProvider, + meta: meta + }); + return views; +}; OKCMS.prototype._createQueries = function(queryConfig, resourceCache) { queryConfig = queryConfig || {}; @@ -218,7 +254,7 @@ ResourceCache.prototype.get = function(type, id) { module.exports = { createApp: function(options) { - return new OKCMS(options); + return OKCMS(options); }, OKResource: OKResource, diff --git a/app/node_modules/okadminview/index.js b/app/node_modules/okadminview/index.js new file mode 100644 index 0000000..42e7f36 --- /dev/null +++ b/app/node_modules/okadminview/index.js @@ -0,0 +1,320 @@ +var bodyParser = require('body-parser'); +var methodOverride = require('method-override'); +var Q = require('q'); +var pluralize = require('pluralize'); +var OKQuery = require('okquery'); + +/** + * OKAdminView! + */ +function OKAdminView(options) { + if (!(this instanceof OKAdminView)) return new OKAdminView(options); + if (!options.app) + throw new Error('No Express app provided to OKAdminView'); + if (!options.express) + throw new Error('No Express provided to OKAdminView'); + if (!options.resourceConfig) + throw new Error('No resourceConfig provided to OKAdminView'); + if (!options.resourceCache) + throw new Error('No resourceCache provided to OKAdminView'); + if (!options.templateProvider) + 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 provider = options.templateProvider; + // Load templates + var templates = this._templates = + ['index', 'resource', 'resource_new'].reduce(function(cache, name) { + var template = provider.getTemplate(name); + if (!template) + throw new Error('Admin theme does not have needed template: "' + name + '"'); + cache[name] = template; + return cache; + }, {}); + // OKAdmin middleware is a router, so mounts on 'use' + Object.defineProperty(this, 'mount', { + value: 'use', + writable: false, + enumerable: true + }); + + // Resources which apper on the index are a function of + // the resource configuration. + var indexQueries = this._indexQueries = Object.keys(resourceConfig) + .map(function(key) { + var config = resourceConfig[key]; + var type = config.type; + var staticData = config.static || {}; + var resource = resourceCache.get(config.type); + if (!resource) + throw new Error('Something weird is going on'); + var id = staticData[resource.idField]; + 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}) + } + }); + + this._middleware = createMiddleware(this); + + function createMiddleware(view) { + var router = express.Router({ + strict: app.get('strict routing') + }); + + // Parse form data + router.use(bodyParser.urlencoded({extended: true})); + // HTML forms only support POST and GET methods + // We extend this by adding hidden input fields to the forms which + // specify the actual method desired. + router.use(methodOverride(function(req, res) { + // Parse out the hidden field + if (req.body && typeof req.body === 'object' && '_method' in req.body) { + var method = req.body._method; + delete req.body._method + return method + } + })); + + router.get('/', function readIndex(req, res, next) { + fetchIndexTemplateData(meta, indexQueries).then(function(data) { + view.renderIndex(req, res, data); + }).fail(errorHandler(req, res)); + }); + + router.get('/:type/new/', function createResourceView(req, res, next) { + var type = req.params.type || ''; + var resource = resourceCache.get(type); + if (!resource) { + 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 + } + }); + }).fail(errorHandler(req, res)); + } + }); + + + router.get('/:type/:id/', function readResource(req, res, next) { + var type = req.params.type || ''; + var id = req.params.id || ''; + var resource = resourceCache.get(type, id); + if (!resource) { + errorHandler(req, res)(new Error('No such resource')); + } else { + var query = OKQuery({ + resource: resource, + query: id + }); + fetchResourceTemplateData(meta, query, transform).then(function(data) { + if (!data) { + resourceMissingHandler(req, res)() + } else { + view.renderResource(req, res, data); + } + }).fail(errorHandler(req, res)); + } + + function transform(meta, resource, data) { + meta = meta || {}; + resource = resource || {}; + data = data || {}; + var spec = Object.keys(resource.spec).reduce(function(cache, prop) { + var propSpec = resource.spec[prop]; + var value = data[prop]; + cache[prop].id = data[resource.idField]; + cache[prop].value = value; + return cache; + }, resource.spec); + return { + meta: meta, + resource: { + type: resource.type, + spec: spec + } + }; + } + }); + + router.post('/:type/', function createResource(req, res, next) { + var type = req.params.type; + var resource = resourceCache.get(type); + var data = req.body; + if (!resource) { + errorHandler(req, res)(new Error('No such resource ' + type)); + } else { + meta.get().then(function(metadata) { + var templateData = { + meta: metadata, + resource: { + type: resource.type, + spec: resource.spec, + data: data + } + }; + try { + resource.assertValid(data); + resource.create(data).then(function(created) { + res.redirect(303, data[resource.idField]); + }).fail(errorHandler(req, res)); + } catch (errors) { + view.renderResource(req, res, templateData); + } + }).fail(errorHandler(req, res));; + } + }); + + router.put('/:type/:id/', function updateResource(req, res, next) { + var type = req.params.type; + var id = req.params.id; + var data = req.body; + var resource = resourceCache.get(type, id); + if (!resource) { + errorHandler(req, res)(new Error('No such resource ' + type)); + } else { + // TODO Maybe should make metadata synchronous... + meta.get().then(function(metadata) { + var templateData = { + meta: metadata, + resource: { + spec: resource.spec, + data: data + } + }; + try { + resource.assertValid(data); + resource.update(id, data).then(function(updated) { + res.redirect(303, '../' + updated[resource.idField]); + }).fail(errorHandler(req, res)); + } catch (errors) { + view.renderResource(req, res, templateData); + } + }).fail(errorHandler(req, res)); + } + }); + + return router; + } +} + +OKAdminView.prototype.middleware = function() { + return this._middleware; +}; + +OKAdminView.prototype.renderIndex = function(req, res, data) { + data = data || {}; + this._templates['index'].render(data).then(function(rendered) { + res.send(rendered); + }).fail(errorHandler(req, res)); +}; + +OKAdminView.prototype.renderResource = function(req, res, data) { + data = data || {}; + this._templates['resource'].render(data).then(function(rendered) { + res.send(rendered); + }).fail(errorHandler(req, res)); +}; + +OKAdminView.prototype.renderResourceNew = function(req, res, data) { + data = data || {meta: {}, resource: {}}; + this._templates['resource_new'].render(data).then(function(rendered) { + res.send(rendered); + }).fail(errorHandler(req, res)); +}; + +/** + * Annotate template data with schema info + */ +function fetchIndexTemplateData(meta, queries) { + return Q.Promise(function(resolve, reject) { + Q.all([meta.get()].concat(queries.map(function(query) { + return query.get(); + }))).then(function(results) { + var meta = results.shift(); + var resources = results.reduce(function(cache, result, i) { + if (!result) + return cache; + var resource = queries[i].resource; + // We want the raw object spec + var spec = resource.spec; + var key = pluralize(resource.type); + if (!cache[key]) { + cache[key] = { + type: resource.type, + spec: spec, + data: [] + }; + } + + if (result.length) { + result.forEach(addToCache) + } else { + addToCache(result); + } + + function addToCache(data) { + // Report id to template under standard name + data.id = data[resource.idField]; + cache[key].data.push(data); + } + + return cache; + }, {}); + + resolve({ + meta: meta, + resources: resources + }); + }).fail(reject); + }); +} + +/** + * Annotate template data with schema info + */ +function fetchResourceTemplateData(meta, query, fn) { + fn = fn || function(m, r, d) { return {meta: m, resource: d}; }; + return Q.Promise(function(resolve, reject) { + meta.get().then(function(metadata) { + query.get().then(function(data) { + var resource = query.resource; + resolve(fn(metadata, resource, data)); + }).fail(reject); + }).fail(reject) + }); +} + +/** + * TODO Real error handling + */ +function errorHandler(req, res) { + return function(err) { + res.send(err.stack); + }; +} + +/** + * TODO Real 404 handling + */ +function resourceMissingHandler(req, res) { + return function() { + res.status(404); + res.send('404'); + } +} + +module.exports = OKAdminView; diff --git a/app/node_modules/okadminview/package.json b/app/node_modules/okadminview/package.json new file mode 100644 index 0000000..b07cb1a --- /dev/null +++ b/app/node_modules/okadminview/package.json @@ -0,0 +1,17 @@ +{ + "name": "okadminview", + "version": "1.0.0", + "description": "administrate!", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "OKFocus", + "license": "None", + "dependencies": { + "body-parser": "^1.12.2", + "method-override": "^2.3.2", + "pluralize": "^1.1.2", + "q": "^1.2.0" + } +} diff --git a/package.json b/package.json index 58f04e1..1e7c1cd 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "license": "None", "dependencies": { "express": "^4.12.3", + "object-assign": "^2.0.0", "q": "^1.2.0" } } diff --git a/themes/okadmin/public/css/main.css b/themes/okadmin/public/css/main.css new file mode 100644 index 0000000..88628e3 --- /dev/null +++ b/themes/okadmin/public/css/main.css @@ -0,0 +1,103 @@ +html, body { + margin: 0; + padding: 0; + font-family: "Helvetica", sans-serif; + font-size: 16px; +} + +ul { + padding: 0; + list-style: none; +} + +a { + color: #0000ff; + text-decoration: none; +} + +a:hover { + border-bottom: 3px solid #0000ff; +} + +a:visited { + color: #0000ff; +} + +.admin-header { + height: 50px; + background-color: rgb(233, 233, 233); +} + +.admin-header .breadcrumb { + margin-left: 2em; + font-size: 2em; + color: rgba(0, 0, 0, 0.25); + line-height: 50px; +} + +.admin-header .site-link { + font-size: 1.5em; + float: right; + margin-right: 10%; + line-height: 50px; +} + +.main.index .resource-category { + float: left; + min-width: 200px; + margin: 1em; + padding: 1em; + background-color: rgba(0, 0, 0, 0.1); +} + +.main.index .resource-category a.add-new { + border-bottom: 3px solid rgba(0, 0, 0, 0); + float: right; + font-size: 1.5em; + color: rgba(0, 0, 0, 0.25); +} + +.main.index .resource-category li { + margin: 1em 0; +} + +.main.index .resource-category a.add-new:hover { + border-bottom: 3px solid rgba(0, 0, 0, 0.25); +} + +.main.resource > * { + margin: 1em 1em; +} + +.main.resource form { + background-color: rgba(0, 0, 0, 0.1); + max-width: 500px; + padding: 1em; + font-size: 1.25em; +} + +.main.resource form label { + display: block; + margin-bottom: 0.25em; + color: rgba(0, 0, 0, 0.75); +} + +.main.resource form .property { + margin: 1em 0; +} + +.main.resource form input { + display: block; + font-size: 1.25em; + min-height: 2em; + padding: 0 0.5em; +} + +.main.resource form button { + font-size: 1.25em; + float: right; +} + +.clear { + clear: both; +} diff --git a/themes/okadmin/templates/index.liquid b/themes/okadmin/templates/index.liquid new file mode 100644 index 0000000..95c64dd --- /dev/null +++ b/themes/okadmin/templates/index.liquid @@ -0,0 +1,26 @@ +{% include 'partials/head' %} + +
+ {% for pair in resources %} + {% assign name = pair[0] %} + {% assign resource = pair[1] %} + {% assign spec = resource.spec %} + +
+
+

{{name | capitalize}}

+
+
    + {% for data in resource.data %} +
  • {{data.id}}
  • + {% endfor %} +
+
+ + +
+
+ + {% endfor %} +
+ +{% include 'partials/tail' %} diff --git a/themes/okadmin/templates/partials/head.liquid b/themes/okadmin/templates/partials/head.liquid new file mode 100644 index 0000000..86915a4 --- /dev/null +++ b/themes/okadmin/templates/partials/head.liquid @@ -0,0 +1,13 @@ + + + + + {{meta.title}} + + + +
+ Admin + View Site +
+
diff --git a/themes/okadmin/templates/partials/inputs.liquid b/themes/okadmin/templates/partials/inputs.liquid new file mode 100644 index 0000000..4dd600d --- /dev/null +++ b/themes/okadmin/templates/partials/inputs.liquid @@ -0,0 +1,19 @@ +{% for pair in resource.spec %} + {% assign name = pair[0] %} + {% assign spec = pair[1] %} + {% assign type = spec.type %} + +
+ {% if type == 'string' %} + + + {% else %} +

Admin template doesn't support '{{type}}' properties!

+ {% endif %} +
+ +{% endfor %} diff --git a/themes/okadmin/templates/partials/tail.liquid b/themes/okadmin/templates/partials/tail.liquid new file mode 100644 index 0000000..773c8d4 --- /dev/null +++ b/themes/okadmin/templates/partials/tail.liquid @@ -0,0 +1,3 @@ +
{% comment %} closes container tag {% endcomment %} + + diff --git a/themes/okadmin/templates/resource.liquid b/themes/okadmin/templates/resource.liquid new file mode 100644 index 0000000..9c1b71c --- /dev/null +++ b/themes/okadmin/templates/resource.liquid @@ -0,0 +1,15 @@ +{% include 'partials/head' %} + +
+ +
+ + {% include 'partials/inputs' %} + +
+
+
+ +{% include 'partials/tail' %} diff --git a/themes/okadmin/templates/resource_new.liquid b/themes/okadmin/templates/resource_new.liquid new file mode 100644 index 0000000..1e414be --- /dev/null +++ b/themes/okadmin/templates/resource_new.liquid @@ -0,0 +1,15 @@ +{% include 'partials/head' %} + +
+ +
+ + {% include 'partials/inputs' %} + +
+
+
+ +{% include 'partials/tail' %} -- cgit v1.2.3-70-g09d2