var path = require('path'); var format = require('util').format; var withTrailingSlash = require('okutil').withTrailingSlash; var withoutTrailingSlash = require('okutil').withoutTrailingSlash; var assign = require('object-assign'); 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'); var OKServer = require('okserver'); var OKSchema = require('okschema'); var OKImageService = require('okservices').OKImageService; require('dotenv').load(); /** * OKCMS! * Basically takes configuration and gives you a server. */ function OKCMS(options) { if (!(this instanceof OKCMS)) return new OKCMS(options); options = options || {}; var app = express(); app.enable('strict routing'); var root = this._root = options.root || 'public'; var adminConfig = options.admin || {}; var adminRoot = this._adminRoot = adminConfig.root || path.join(__dirname, '../themes/okadmin/public'); var adminPath = this._adminPath = adminConfig.path || '/_admin' var templateRoot = options.templateRoot || 'templates'; var adminTemplateRoot = options.templateRoot || path.join(__dirname, '../themes/okadmin/templates'); // Set metadata defaults // TODO Abstract this out somewhere else var meta = { type: 'meta', get: function() { return Q.promise(function(resolve, reject) { db.getMeta().then(function(metadata) { resolve(assign({}, { static: '' }, metadata)); }).fail(reject); }); } }; var adminMeta ={ type: 'meta', get: function() { return Q.promise(function(resolve, reject) { db.getMeta().then(function(metadata) { resolve(assign({}, { static: withoutTrailingSlash(adminPath) }, metadata)); }).fail(reject); }); } }; var schemaConfig = options.schemas || {}; var resourceConfig = options.resources || []; var viewConfig = options.views || { '/': { template: 'index' } }; var serviceConfig = options.services || {}; var templateProvider = this._templateProvider = new OKTemplate({root: templateRoot}); var adminTemplateProvider = this._adminTemplateProvider = new OKTemplate({root: adminTemplateRoot}); var db = new OKDB(options.db || 'fs'); var schemas = this._schemas = this._createSchemas(schemaConfig); var resourceCache = this._resourceCache = this._createResources(resourceConfig, db, schemas); // 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); // Create services var imageService = OKImageService({ express: express, s3: serviceConfig.s3, }); var server = this._server = new OKServer({ express: express, app: app, // 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, services: { image: imageService } }); } OKCMS.prototype.listen = function listen(port, options) { options = options || {}; this._server.listen(port); }; OKCMS.prototype._createSchemas = function(schemaConfig) { schemaConfig = schemaConfig || {}; return Object.keys(schemaConfig).reduce(function(cache, key) { var spec = schemaConfig[key]; cache[key] = OKSchema(spec); return cache; }, {}); } OKCMS.prototype._createResources = function(resourceConfig, db, schemaCache) { resourceConfig = resourceConfig || {}; var resources = resourceConfig.map(function(config) { var type = config.type; var schema = schemaCache[type]; if (!schema) throw new Error('Resource config references nonexistent schema'); var resource = OKResource({ type: type, db: db, schema: schema }); // 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, meta, resourceCache, templateProvider) { viewConfig = viewConfig || {}; var self = this; var createQueries = this._createQueries.bind(this); return Object.keys(viewConfig).reduce(function(cache, route) { var config = viewConfig[route]; var templateName = config.template || getDefaultTemplate(route, config); var template = templateProvider.getTemplate(templateName); if (!template) { throw new Error(format('No template named "%s" found', templateName)); } var queryConfig = config.data || []; var queries = createQueries(queryConfig, resourceCache); // Don't forget to add that trailing slash if the user forgot cache[withTrailingSlash(route)] = OKView({ mount: 'get', // User defined views are read only route: route, template: template, queries: queries, meta: meta }); return cache; }, {}); /** * Returns the default template for a view config */ function getDefaultTemplate(route, config) { // Root route defaults to index if (/^\/?$/.test(route)) return 'index'; // Otherwise default to the backing resource name else if (config && config.data && config.data.type) return config.data.type; // Otherwise dunno 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 || {}; if (!queryConfig.length) queryConfig = [queryConfig]; return queryConfig.map(function(config) { var type = config.type; var resource = resourceCache.get(type, config.query); if (!resource) throw new Error('Query configured with nonexistent resource'); // Default to "select all" query var query = config.query || '*'; return new OKQuery({ resource: resource, query: query }); }); }; /** * 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) { return OKCMS(options); }, OKResource: OKResource, OKView: OKView };