summaryrefslogtreecommitdiff
path: root/app/index.js
blob: b9958c2563ebd35104508852877fe7bdb2db34df (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
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;

/**
 * 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 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
  });

  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

};