Compile all Coco files, otherwise we don't have Middleware :P middleware
authorDavid Schoonover <dsc@wikimedia.org>
Tue, 10 Jul 2012 13:33:27 +0000 (06:33 -0700)
committerDavid Schoonover <dsc@wikimedia.org>
Tue, 10 Jul 2012 13:33:33 +0000 (06:33 -0700)
23 files changed:
Cokefile
lib/base/asset-manager.js [new file with mode: 0644]
lib/chart/type/d3/d3-bar-chart-type.js [new file with mode: 0644]
lib/chart/type/d3/d3-geo-element.js [new file with mode: 0644]
lib/server/controller.js [new file with mode: 0644]
lib/server/controllers/dashboard.js [new file with mode: 0644]
lib/server/controllers/datasource.js [new file with mode: 0644]
lib/server/controllers/graph.js [new file with mode: 0644]
lib/server/controllers/index.js [new file with mode: 0644]
lib/server/file-controller.js [new file with mode: 0644]
lib/server/files.js [new file with mode: 0644]
lib/server/middleware.js [new file with mode: 0644]
lib/server/mkdirp.js [new file with mode: 0644]
lib/server/proxy.js [new file with mode: 0644]
lib/server/reqinfo.js [new file with mode: 0644]
lib/server/server.js [new file with mode: 0644]
lib/server/view-helpers.js [new file with mode: 0644]
lib/util/aliasdict.js [new file with mode: 0644]
lib/util/bitstring.js [new file with mode: 0644]
lib/util/crc.js [new file with mode: 0644]
lib/util/hashset.js [new file with mode: 0644]
src/util/aliasdict.co
src/util/underscore/_functions.co [deleted file]

index 1b23cd0..4e13b86 100644 (file)
--- a/Cokefile
+++ b/Cokefile
@@ -74,18 +74,13 @@ task \build 'Build coco sources' ->
             """
             @ok()
         
-        .set glob('src/main-*.co', {+sync}).concat sources("www/modules.yaml", 'development').map -> it.slice 1
-        .seqEach (srcfile) ->
-            infile = srcfile.replace /^js\/kraken/, 'src' .replace matchExt, '.co'
+        .set glob 'src/**/*.co', {+sync}
+        .seqEach (infile) ->
             return @ok() unless exists infile
-            # unless exists infile
-            #     console.log "  Skipping Coco compile:\t (#srcfile)\t #infile does not exist"
-            #     return @ok()
-            
-            outfile = srcfile.replace /^(js\/kraken|src)/, 'lib' .replace matchExt, '.js' .replace /\.co$/, '.js'
+            outfile = infile.replace /^src/, 'lib' .replace /\.co$/, '.js'
             console.log "  Compiling Coco to JS:\t #infile \t-->\t #outfile"
             mkdirp dirname outfile
-            write outfile, Coco.compile read(infile), {+bare}
+            write outfile, Coco.compile read(infile), {+bare, filename:infile}
             @ok()
         
         .set sources("www/modules.yaml", 'development').map -> it.slice 1
diff --git a/lib/base/asset-manager.js b/lib/base/asset-manager.js
new file mode 100644 (file)
index 0000000..d65ef1e
--- /dev/null
@@ -0,0 +1,49 @@
+var op, ReadyEmitter, AssetManager, _ref, _;
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+ReadyEmitter = require('kraken/util/event').ReadyEmitter;
+AssetManager = (function(superclass){
+  AssetManager.displayName = 'AssetManager';
+  var prototype = __extend(AssetManager, superclass).prototype, constructor = AssetManager;
+  prototype.assets = null;
+  /**
+   * @constructor
+   */;
+  function AssetManager(){
+    superclass.apply(this, arguments);
+    this.assets = {};
+  }
+  /**
+   * Load the corresponding chart specification, which includes
+   * info about valid options, along with their types and defaults.
+   */
+  prototype.load = function(){
+    var proto, _this = this;
+    if (this.ready) {
+      return this;
+    }
+    proto = this.constructor.prototype;
+    jQuery.ajax({
+      url: this.SPEC_URL,
+      success: function(spec){
+        proto.spec = spec;
+        proto.options_ordered = spec;
+        proto.options = _.synthesize(spec, function(it){
+          return [it.name, it];
+        });
+        proto.ready = true;
+        return _this.emit('ready', _this);
+      },
+      error: function(it){
+        return console.error("Error loading " + _this.typeName + " spec! " + it);
+      }
+    });
+    return this;
+  };
+  return AssetManager;
+}(ReadyEmitter));
+function __extend(sub, sup){
+  function fun(){} fun.prototype = (sub.superclass = sup).prototype;
+  (sub.prototype = new fun).constructor = sub;
+  if (typeof sup.extended == 'function') sup.extended(sub);
+  return sub;
+}
\ No newline at end of file
diff --git a/lib/chart/type/d3/d3-bar-chart-type.js b/lib/chart/type/d3/d3-bar-chart-type.js
new file mode 100644 (file)
index 0000000..fa0746a
--- /dev/null
@@ -0,0 +1,138 @@
+var d3, op, ChartType, root, BarChartType, _ref, _;
+d3 = require('d3');
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+ChartType = require('kraken/chart/chart-type').ChartType;
+root = function(){
+  return this;
+}();
+exports.BarChartType = BarChartType = (function(superclass){
+  BarChartType.displayName = 'BarChartType';
+  var prototype = __extend(BarChartType, superclass).prototype, constructor = BarChartType;
+  prototype.__bind__ = ['determineSize'];
+  prototype.SPEC_URL = '/schema/d3/d3-bar.json';
+  prototype.typeName = 'd3-bar';
+  ChartType.register(BarChartType);
+  /**
+   * Hash of role-names to the selector which, when applied to the view,
+   * returns the correct element.
+   * @type Object
+   */
+  prototype.roles = {
+    viewport: '.viewport',
+    legend: '.graph-legend'
+  };
+  function BarChartType(){
+    superclass.apply(this, arguments);
+  }
+  prototype.getData = function(){
+    return this.model.dataset.getColumns();
+  };
+  prototype.transform = function(){
+    var dataset, options;
+    dataset = this.model.dataset;
+    options = __import(this.model.getOptions(), this.determineSize());
+    __import(options, {
+      colors: dataset.getColors(),
+      labels: dataset.getLabels()
+    });
+    return options;
+  };
+  prototype.renderChartType = function(metric, svgEl, xScale, yScale){
+    var X, Y, metricBars, data, barWidth, barHeight, chT;
+    X = function(d, i){
+      return xScale(d[0]);
+    };
+    Y = function(d, i){
+      return yScale(d[1]);
+    };
+    metricBars = root.metricBars = svgEl.append("g").attr("class", "metric bars " + metric.get('label'));
+    data = d3.zip(metric.getDateColumn(), metric.getData());
+    barWidth = svgEl.attr('width') / data.length;
+    barHeight = function(d){
+      return svgEl.attr('height') - Y(d);
+    };
+    metricBars.selectAll("bar").data(data).enter().append("rect").attr("class", function(d, i){
+      return "metric bar " + i;
+    }).attr("x", X).attr("y", Y).attr("height", barHeight).attr("width", function(){
+      return barWidth;
+    }).attr("fill", metric.get('color')).attr("stroke", "white").style("opacity", "0.4").style("z-index", -10);
+    chT = this;
+    metricBars.selectAll(".metric.bar").on("mouseover", function(d, i){
+      return svgEl.append("text").attr("class", "mf").attr("dx", 50).attr("dy", 100).style("font-size", "0px").transition().duration(800).text("Uh boy, the target would be:  " + chT.numberFormatter(d[1]).toString()).style("font-size", "25px");
+    }).on("mouseout", function(d, i){
+      return svgEl.selectAll(".mf").transition().duration(300).text("BUMMER!!!").style("font-size", "0px").remove();
+    });
+    return svgEl;
+  };
+  prototype.renderChart = function(data, viewport, options, lastChart){
+    var margin, width, height, xScale, yScale, dates, cols, allValues, svg, enterFrame, frame, xAxis, X, Y, barWidth, barHeight, bars, lens, gLens, gInner, mf;
+    margin = {
+      top: 20,
+      right: 20,
+      bottom: 20,
+      left: 20
+    };
+    width = 760;
+    height = 320;
+    xScale = d3.time.scale();
+    yScale = d3.scale.linear();
+    dates = data[0];
+    cols = data.slice(1);
+    allValues = d3.merge(cols);
+    xScale.domain(d3.extent(dates)).range([0, width - margin.left - margin.right]);
+    yScale.domain(d3.extent(allValues)).range([height - margin.top - margin.bottom, 0]);
+    svg = d3.select(viewport[0]).selectAll("svg").data([cols]);
+    enterFrame = svg.enter().append("svg").append("g").attr("class", "frame");
+    enterFrame.append("g").attr("class", "x axis time");
+    svg.attr("width", width).attr("height", height);
+    frame = svg.select("g.frame").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+    xAxis = d3.svg.axis().scale(xScale).orient("bottom").tickSize(6, 0);
+    frame.select(".x.axis.time").attr("transform", "translate(0," + yScale.range()[0] + ")").call(xAxis);
+    X = function(d, i){
+      return xScale(d[0]);
+    };
+    Y = function(d, i){
+      return yScale(d[1]);
+    };
+    barWidth = svg.attr('width') / dates.length;
+    barHeight = function(d){
+      return svg.attr('height') - Y(d);
+    };
+    bars = frame.selectAll("g.bars").data(cols.map(function(it){
+      return d3.zip(dates, it);
+    }));
+    bars.enter().append("g").attr("class", function(col, i){
+      return "metric bars " + i;
+    });
+    bars.exit().remove();
+    bars.selectAll(".bar").data(op.first).enter().append("rect").attr("class", "bar").attr("x", X).attr("y", Y).attr("height", barHeight).attr("width", function(){
+      return barWidth;
+    }).attr("fill", "red").attr("stroke", "white");
+    lens = root.lens = frame.selectAll("g.lens").data([[]]);
+    gLens = lens.enter().append("g").attr("class", "lens").style("z-index", 1e9);
+    gInner = gLens.append("g").attr("transform", "translate(1.5em,0)");
+    gInner.append("circle").attr("r", "1.5em").style("fill", "rgba(255, 255, 255, 0.4)").style("stroke", "white").style("stroke-width", "3px");
+    gInner.append("text").attr("y", "0.5em").attr("text-anchor", "middle").style("fill", "white").style("font", "12px Helvetica").style("font-weight", "bold");
+    mf = frame.selectAll("g.mf").data(["mf"]).enter().append("g").attr("class", "mf").append("text").attr("class", "yoyo").attr("dx", 50).attr("dy", 100);
+    bars.selectAll(".bar").on("mouseover", function(d, i){
+      var el;
+      el = root.el = el;
+      return mf.transition().duration(300).ease("exp").text("Uh boy, the target would be:" + d[1]).style("font-size", "25px");
+    }).on("mouseout", function(d, i){
+      return mf.transition().duration(1000).text("BUMMER!!!").style("font-size", "0px");
+    });
+    return svg;
+  };
+  return BarChartType;
+}(ChartType));
+function __extend(sub, sup){
+  function fun(){} fun.prototype = (sub.superclass = sup).prototype;
+  (sub.prototype = new fun).constructor = sub;
+  if (typeof sup.extended == 'function') sup.extended(sub);
+  return sub;
+}
+function __import(obj, src){
+  var own = {}.hasOwnProperty;
+  for (var key in src) if (own.call(src, key)) obj[key] = src[key];
+  return obj;
+}
\ No newline at end of file
diff --git a/lib/chart/type/d3/d3-geo-element.js b/lib/chart/type/d3/d3-geo-element.js
new file mode 100644 (file)
index 0000000..64ddb6c
--- /dev/null
@@ -0,0 +1,131 @@
+var ColorBrewer, op, ChartType, GeoWorldChartType, data, main, _ref, _;
+ColorBrewer = require('colorbrewer');
+_ref = require('kraken/util'), _ = _ref._, op = _ref.op;
+ChartType = require('kraken/chart/chart-type').ChartType;
+exports.GeoWorldChartType = GeoWorldChartType = (function(superclass){
+  GeoWorldChartType.displayName = 'GeoWorldChartType';
+  var prototype = __extend(GeoWorldChartType, superclass).prototype, constructor = GeoWorldChartType;
+  prototype.__bind__ = ['dygNumberFormatter', 'dygNumberFormatterHTML'];
+  prototype.SPEC_URL = '/schema/d3/d3-geo-world.json';
+  prototype.typeName = 'd3-geo-world';
+  ChartType.register(GeoWorldChartType);
+  /**
+   * Hash of role-names to the selector which, when applied to the view,
+   * returns the correct element.
+   * @type Object
+   */
+  prototype.roles = {
+    viewport: '.viewport',
+    legend: '.graph-legend'
+  };
+  function GeoWorldChartType(){
+    superclass.apply(this, arguments);
+  }
+  prototype.transform = function(){
+    var options;
+    options = __import(this.model.getOptions(), this.determineSize());
+    if (options.colors.scaleDomain != null) {
+      options.colors.scaleDomain = d3.extent;
+    }
+    return options;
+  };
+  prototype.getProjection = function(type){
+    switch (type) {
+    case 'mercator':
+    case 'albers':
+    case 'albersUsa':
+      return d3.geo[type]();
+    case 'azimuthalOrtho':
+      return d3.geo.azimuthal().mode('orthographic');
+    case 'azimuthalStereo':
+      return d3.geo.azimuthal().mode('stereographic');
+    default:
+      throw new Error("Invalid map projection type '" + type + "'!");
+    }
+  };
+  prototype.renderChart = function(data, viewport, options, lastChart){
+    var width, height, fill, quantize, projection, path, feature, infobox, move, zoom, chart, setInfoBox, worldmap;
+    width = options.width, height = options.height;
+    fill = this.fill = function(data, options){
+      return d3.scale[options.colors.scale]().domain(options.colors.scaleDomain).range(options.colors.palette);
+    };
+    quantize = this.quantize = function(data, options){
+      return function(d){
+        if (data[d.properties.name] != null) {
+          return fill(data[d.properties.name].editors);
+        } else {
+          return fill("rgb(0,0,0)");
+        }
+      };
+    };
+    projection = this.projection = this.getProjection(options.map.projection).scale(width).translate([width / 2, height / 2]);
+    path = d3.geo.path().projection(projection);
+    feature = map.selectAll(".feature");
+    infobox = d3.select('.infobox');
+    move = function(){
+      projection.translate(d3.event.translate).scale(d3.event.scale);
+      return feature.attr("d", path);
+    };
+    zoom = d3.behavior.zoom().translate(projection.translate()).scale(projection.scale()).scaleExtent([height, height * 8]).on("zoom", move);
+    chart = d3.select(viewport[0]).append("svg:svg").attr("width", width).attr("height", height).append("svg:g").attr("transform", "translate(0,0)").call(zoom);
+    map.append("svg:rect").attr("class", "frame").attr("width", width).attr("height", height);
+    infobox.select('#ball').append("svg:svg").attr("width", "100%").attr("height", "20px").append("svg:rect").attr("width", "60%").attr("height", "20px").attr("fill", '#f40500');
+    setInfoBox = function(d){
+      var name, ae, e5, e100, xy;
+      name = d.properties.name;
+      ae = 0;
+      e5 = 0;
+      e100 = 0;
+      if (data[name] != null) {
+        ae = parseInt(data[name].editors);
+        e5 = parseInt(data[name].editors5);
+        e100 = parseInt(data[name].editors100);
+      }
+      infobox.select('#country').text(name);
+      infobox.select('#ae').text(ae);
+      infobox.select('#e5').text(e5 + " (" + (100.0 * e5 / ae).toPrecision(3) + "%)");
+      infobox.select('#e100').text(e100 + " (" + (100.0 * e100 / ae).toPrecision(3) + "%)");
+      xy = d3.svg.mouse(this);
+      infobox.style("left", xy[0] + 'px');
+      infobox.style("top", xy[1] + 'px');
+      return infobox.style("display", "block");
+    };
+    return worldmap = function(){
+      return d3.json("/data/geo/maps/world-countries.json", function(json){
+        return feature = feature.data(json.features).enter().append("svg:path").attr("class", "feature").attr("d", path).attr("fill", quantize).attr("id", function(d){
+          return d.properties.name;
+        }).on("mouseover", setInfoBox).on("mouseout", function(){
+          return infobox.style("display", "none");
+        });
+      });
+    };
+  };
+  return GeoWorldChartType;
+}(ChartType));
+data = null;
+main = function(){
+  return jQuery.ajax({
+    url: "/data/geo/data/en_geo_editors.json",
+    dataType: 'json',
+    success: function(res){
+      data = res;
+      jQuery('.geo-spinner').spin(false).hide();
+      worldmap();
+      return console.log('Loaded geo coding map!');
+    },
+    error: function(err){
+      return console.error(err);
+    }
+  });
+};
+function __extend(sub, sup){
+  function fun(){} fun.prototype = (sub.superclass = sup).prototype;
+  (sub.prototype = new fun).constructor = sub;
+  if (typeof sup.extended == 'function') sup.extended(sub);
+  return sub;
+}
+function __import(obj, src){
+  var own = {}.hasOwnProperty;
+  for (var key in src) if (own.call(src, key)) obj[key] = src[key];
+  return obj;
+}
\ No newline at end of file
diff --git a/lib/server/controller.js b/lib/server/controller.js
new file mode 100644 (file)
index 0000000..2c0f556
--- /dev/null
@@ -0,0 +1,117 @@
+var express, Resource, hasOwn, Controller, exports;
+express = require('express');
+Resource = require('express-resource');
+hasOwn = Object.prototype.hasOwnProperty;
+/**
+ * @class Resource controller for easily subclassing an express Resource.
+ */
+Controller = (function(superclass){
+  Controller.displayName = 'Controller';
+  var prototype = __extend(Controller, superclass).prototype, constructor = Controller;
+  /**
+   * Singular, lowercase resource-noun.
+   * @optional
+   * @type String
+   * @example "user"
+   */
+  prototype.id = null;
+  /**
+   * Plural, lowercase resource-noun.
+   * @required
+   * @type String
+   * @example "users"
+   */
+  prototype.name = null;
+  /**
+   * Resource routing prefix.
+   * @optional
+   * @type String
+   */
+  prototype.base = '/';
+  /**
+   * Default format.
+   * @type String
+   */
+  prototype.format = null;
+  /**
+   * Hash of sub-routes. Keys are routes, and values are either:
+   *  - String: the name of a method to be used for used for all HTTP-methods
+   *    for this sub-route.
+   *  - Object: Hash of HTTP-method (get, put, post, del, or all) to the name
+   *    of a method on this Controller.
+   * 
+   * Example:
+   *      { '/foo' => 'foo',
+   *        '/bar' => {'get' => 'get_bar', 'del' => 'delete_bar' },
+   *      }
+   *      If this mapping is in a controller with name 'nonya', then
+   *          GET     '/nonya/foo' -> NonyaController.foo(),
+   *          GET     '/nonya/bar' -> NonyaController.get_bar()
+   *          DELETE  '/nonya/bar' -> NonyaController.delete_bar()
+   * 
+   * @type Object
+   */
+  prototype.mapping = null;
+  /**
+   * @constructor
+   */;
+  function Controller(app, name){
+    var actions, k, fn, _ref;
+    this.app = app;
+    this.routes || (this.routes = {});
+    actions = {};
+    for (k in this) {
+      fn = this[k];
+      if (!(typeof fn === 'function' && k !== 'constructor')) {
+        continue;
+      }
+      actions[k] = this[k] = fn.bind(this);
+    }
+    delete actions.load;
+    if (typeof actions.autoload === 'function') {
+      actions.load = actions.autoload;
+    }
+    ((_ref = this.app).resources || (_ref.resources = {}))[this.name] = this;
+    this.applyControllerMapping();
+    superclass.call(this, name || this.name, actions, this.app);
+  }
+  /**
+   * Apply the contents of a mapping hash.
+   * @private
+   */
+  prototype.applyControllerMapping = function(mapping){
+    var subroute, methods, verb, method;
+    mapping == null && (mapping = this.mapping);
+    for (subroute in mapping) {
+      methods = mapping[subroute];
+      if (typeof methods === 'string') {
+        methods = {
+          all: methods
+        };
+      }
+      for (verb in methods) {
+        method = methods[verb];
+        this.map(verb, subroute, this[method]);
+      }
+    }
+    return this;
+  };
+  prototype.toString = function(){
+    return this.constructor.name + "('" + this.name + "', base='" + this.base + "', app=" + this.app + ")";
+  };
+  return Controller;
+}(Resource));
+express.HTTPServer.prototype.controller = express.HTTPSServer.prototype.controller = function(name, ControllerClass, opts){
+  var _ref;
+  if (typeof name === 'function') {
+    _ref = [ControllerClass, name, null], opts = _ref[0], ControllerClass = _ref[1], name = _ref[2];
+  }
+  return new ControllerClass(this, name);
+};
+module.exports = exports = Controller;
+function __extend(sub, sup){
+  function fun(){} fun.prototype = (sub.superclass = sup).prototype;
+  (sub.prototype = new fun).constructor = sub;
+  if (typeof sup.extended == 'function') sup.extended(sub);
+  return sub;
+}
\ No newline at end of file
diff --git a/lib/server/controllers/dashboard.js b/lib/server/controllers/dashboard.js
new file mode 100644 (file)
index 0000000..8e48b26
--- /dev/null
@@ -0,0 +1,102 @@
+var fs, path, exists, Seq, yaml, mkdirp, mkdirpAsync, readJSONFilesAsync, FileBackedController, DashboardController, exports, _, _ref;
+fs = require('fs');
+path = require('path');
+exists = path.existsSync;
+_ = require('underscore');
+Seq = require('seq');
+yaml = require('js-yaml');
+_ref = require('../mkdirp'), mkdirp = _ref.mkdirp, mkdirpAsync = _ref.mkdirpAsync;
+readJSONFilesAsync = require('../files').readJSONFilesAsync;
+FileBackedController = require('../file-controller');
+/**
+ * @class Resource controller for dashboard requests.
+ */
+DashboardController = (function(superclass){
+  DashboardController.displayName = 'DashboardController';
+  var prototype = __extend(DashboardController, superclass).prototype, constructor = DashboardController;
+  prototype.PROTECTED_IDS = ['main', 'reportcard'];
+  prototype.PROTECT = true;
+  prototype.name = 'dashboards';
+  prototype.dataDir = 'data/dashboards';
+  function DashboardController(){
+    superclass.apply(this, arguments);
+  }
+  prototype.index = function(req, res){
+    var pattern;
+    switch (req.format) {
+    case 'json':
+      pattern = this.dataDir + "/*.json";
+      return Seq().seq(function(){
+        return readJSONFilesAsync(pattern, this);
+      }).seq(function(files){
+        return res.send(_.values(files));
+      });
+    default:
+      return res.render(this.id + "/index");
+    }
+  };
+  prototype.show = function(req, res){
+    if (req.format === 'json') {
+      return res.send(req.dashboard);
+    } else {
+      return res.render(this.id + "/view");
+    }
+  };
+  prototype.edit = function(req, res){
+    if (req.format === 'json') {
+      return res.send(req.dashboard);
+    } else {
+      return res.render(this.id + "/edit");
+    }
+  };
+  prototype['new'] = function(req, res){
+    return res.render(this.id + "/edit");
+  };
+  prototype.create = function(req, res){
+    var data, file;
+    if (!(data = this.processBody(req, res))) {
+      return;
+    }
+    file = this.toFile(data.id);
+    if (exists(file)) {
+      return res.send({
+        result: "error",
+        message: "Dashboard '" + data.id + "' already exists!"
+      });
+    } else {
+      return fs.writeFile(file, JSON.stringify(data), "utf8", this.errorHandler(res, "Error writing Dashboard!"));
+    }
+  };
+  prototype.update = function(req, res){
+    var data;
+    if (!(data = this.processBody(req, res))) {
+      return;
+    }
+    if (this.PROTECT && _(this.PROTECTED_IDS).contains(data.id)) {
+      return res.send({
+        result: "error",
+        message: "Dashboard '" + data.id + "' is read-only."
+      }, 403);
+    }
+    return fs.writeFile(this.toFile(data.id), JSON.stringify(data), "utf8", this.errorHandler(res, "Error writing Dashboard!"));
+  };
+  prototype.destroy = function(req, res){
+    var id;
+    id = req.param.dashboard;
+    if (this.PROTECT && _(this.PROTECTED_IDS).contains(id)) {
+      return res.send({
+        result: "error",
+        message: "Dashboard '" + id + "' is read-only."
+      }, 403);
+    }
+    return fs.unlink(this.toFile(id), this.errorHandler(res, "Dashboard '" + id + "' does not exist!"));
+  };
+  return DashboardController;
+}(FileBackedController));
+module.exports = exports = DashboardController;
+function __extend(sub, sup){
+  function fun(){} fun.prototype = (sub.superclass = sup).prototype;
+  (sub.prototype = new fun).constructor = sub;
+  if (typeof sup.extended == 'function') sup.extended(sub);
+  return sub;
+}
\ No newline at end of file
diff --git a/lib/server/controllers/datasource.js b/lib/server/controllers/datasource.js
new file mode 100644 (file)
index 0000000..3373cfe
--- /dev/null
@@ -0,0 +1,146 @@
+var fs, path, exists, yaml, findit, Seq, Controller, EXT_PAT, YAML_EXT_PAT, YAML_OR_JSON_PAT, DataSourceController, exports, _;
+fs = require('fs');
+path = require('path');
+exists = path.existsSync;
+_ = require('underscore');
+yaml = require('js-yaml');
+findit = require('findit');
+Seq = require('seq');
+Controller = require('../controller');
+EXT_PAT = /\.[^\.]*$/i;
+YAML_EXT_PAT = /\.ya?ml$/i;
+YAML_OR_JSON_PAT = /\.(json|ya?ml)$/i;
+/**
+ * @class Resource controller for graph requests.
+ */
+DataSourceController = (function(superclass){
+  DataSourceController.displayName = 'DataSourceController';
+  var prototype = __extend(DataSourceController, superclass).prototype, constructor = DataSourceController;
+  prototype.name = 'datasources';
+  prototype.dataDir = 'data/datasources';
+  prototype.mapping = {
+    all: 'allData'
+  };
+  function DataSourceController(){
+    superclass.apply(this, arguments);
+  }
+  prototype.toFile = function(id){
+    return this.dataDir + "/" + id + ".json";
+  };
+  /**
+   * Auto-load :id for related requests.
+   */
+  prototype.autoload = function(id, cb){
+    var files, pattern, file, parser;
+    files = findit.sync(this.dataDir);
+    pattern = new RegExp(id + ".(json|ya?ml)$", "i");
+    file = _.find(files, function(it){
+      return pattern.test(it);
+    });
+    if (!file) {
+      console.error("Unable to find DataSource for '" + id + "'!");
+      return cb(new Error("Unable to find DataSource for '" + id + "'!"));
+    }
+    if (_.endsWith(file, id + ".json")) {
+      parser = JSON.parse;
+    }
+    if (_.endsWith(file, id + ".yaml")) {
+      parser = yaml.load;
+    }
+    return fs.readFile(file, 'utf8', function(err, data){
+      if ('ENOENT' === (err != null ? err.code : void 8)) {
+        console.error("Unable to find DataSource for '" + id + "'!");
+        return cb(new Error("Unable to find DataSource for '" + id + "'!"));
+      }
+      if (err) {
+        console.error("DataSourceController.autoload(" + id + ", " + typeof cb + ") -->\n", err);
+        return cb(err);
+      }
+      try {
+        return cb(null, parser(data));
+      } catch (err) {
+        console.error("DataSourceController.autoload(" + id + ", " + typeof cb + ") -->\n", err);
+        return cb(err);
+      }
+    });
+  };
+  /**
+   * GET /datasources
+   * @returns {Object} JSON listing of the datasource metadata files.
+   */
+  prototype.index = function(req, res, next){
+    var files;
+    files = findit.sync(this.dataDir);
+    return res.send(files.filter(function(it){
+      return YAML_OR_JSON_PAT.test(it);
+    }).map(function(it){
+      return (it + "").replace(YAML_EXT_PAT, '.json');
+    }));
+  };
+  /**
+   * GET /datasources/:datasource
+   */
+  prototype.show = function(req, res){
+    return res.send(req