summaryrefslogtreecommitdiff
path: root/app/index.js
blob: 0e83363c696392049703554e6559fc2cec7b1611 (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
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 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

};