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 OKQuery = require('okquery'); var OKView = require('okview'); var OKDB = require('okdb'); var OKResource = require('okresource') var OKTemplate = require('oktemplate'); var OKServer = require('okserver'); var OKRestEndpoint = require('okrest'); var OKSchema = require('okschema'); /** * 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'); var schemaConfig = options.schemas || {}; var resourceConfig = options.resources || []; var viewConfig = options.views || { '/': { template: 'index' } }; var templateProvider = this._templateProvider = new OKTemplate({root: templateRoot}); var adminTemplateProvider = this._adminTemplateProvider = new OKTemplate({root: adminTemplateRoot}); var db = new OKDB(options.db || 'fs'); // Special query to get project wide meta data var meta = this._meta = { type: 'meta', get: function() { return db.getMeta(); } }; 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 server = this._server = new OKServer({ express: express, app: app, views: views, root: root, adminRoot: adminRoot, adminPath: adminPath }); } 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._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); 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 new OKCMS(options); }, OKResource: OKResource, OKView: OKView, OKRestEndpoint: OKRestEndpoint };