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.datasource);
+  };
+  /**
+   * Returns the aggregated JSON content of the datasource metadata files.
+   */
+  prototype.allData = function(req, res, next){
+    var data, files, _this = this;
+    data = {};
+    files = [];
+    return Seq(findit.sync(this.dataDir)).filter(function(it){
+      return YAML_OR_JSON_PAT.test(it);
+    }).seq(function(){
+      files = this.stack.slice();
+      return this.ok(files);
+    }).flatten().parMap_(function(next, f){
+      return fs.readFile(f, 'utf8', next);
+    }).parMap(function(text, i){
+      var f, k, v, that;
+      f = files[i];
+      k = f.replace(YAML_EXT_PAT, '.json');
+      v = data[k] = {};
+      try {
+        if (YAML_EXT_PAT.test(f)) {
+          v = data[k] = yaml.load(text);
+        } else {
+          v = data[k] = JSON.parse(text);
+        }
+        return this.ok(v);
+      } catch (err) {
+        console.error("[/datasources] catch! " + err);
+        console.error(err);
+        if (that = err.stack) {
+          console.error(that);
+        }
+        return res.send({
+          error: String(err),
+          partial_data: data
+        });
+      }
+    }).seq(function(){
+      return res.send(data);
+    })['catch'](function(err){
+      var that;
+      console.error('[/datasources] catch!');
+      console.error(err);
+      if (that = err.stack) {
+        console.error(that);
+      }
+      return res.send({
+        error: String(err),
+        partial_data: data
+      });
+    });
+  };
+  return DataSourceController;
+}(Controller));
+module.exports = exports = DataSourceController;
+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/graph.js b/lib/server/controllers/graph.js
new file mode 100644 (file)
index 0000000..13a89d2
--- /dev/null
@@ -0,0 +1,174 @@
+var fs, path, exists, Seq, yaml, mkdirp, mkdirpAsync, readJSONFilesAsync, Controller, GraphController, 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;
+Controller = require('../controller');
+/**
+ * @class Resource controller for graph requests.
+ */
+GraphController = (function(superclass){
+  GraphController.displayName = 'GraphController';
+  var prototype = __extend(GraphController, superclass).prototype, constructor = GraphController;
+  prototype.PROTECTED_GRAPH_IDS = ['unique_visitors', 'pageviews', 'pageviews_mobile', 'reach', 'commons', 'articles', 'articles_per_day', 'edits', 'new_editors', 'active_editors', 'active_editors_target', 'very_active_editors'];
+  prototype.PROTECT_GRAPHS = true;
+  prototype.name = 'graphs';
+  prototype.dataDir = 'data/graphs';
+  function GraphController(){
+    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 file, parser, yamlFile;
+    file = this.toFile(id);
+    parser = JSON.parse;
+    yamlFile = file.replace(/\.json$/i, '.yaml');
+    if (exists(yamlFile)) {
+      file = yamlFile;
+      parser = yaml.load;
+    }
+    return fs.readFile(file, 'utf8', function(err, data){
+      if ('ENOENT' === (err != null ? err.code : void 8)) {
+        return cb(null, {});
+      }
+      if (err) {
+        console.error("GraphController.autoload(" + id + ", " + typeof cb + ") -->\nerr");
+        return cb(err);
+      }
+      try {
+        return cb(null, parser(data));
+      } catch (err) {
+        console.error("GraphController.autoload(" + id + ", " + typeof cb + ") -->\nerr");
+        return cb(err);
+      }
+    });
+  };
+  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(graphs){
+        return res.send(_.values(graphs));
+      });
+    default:
+      return res.render('graph/index');
+    }
+  };
+  prototype.show = function(req, res){
+    if (req.format === 'json') {
+      return res.send(req.graph);
+    } else {
+      return res.render('graph/view');
+    }
+  };
+  prototype.edit = function(req, res){
+    if (req.format === 'json') {
+      return res.send(req.graph);
+    } else {
+      return res.render('graph/edit');
+    }
+  };
+  prototype['new'] = function(req, res){
+    return res.render('graph/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: "Graph '" + data.id + "' already exists!"
+      });
+    } else {
+      return fs.writeFile(file, JSON.stringify(data), "utf8", this.errorHandler(res, "Error writing graph!"));
+    }
+  };
+  prototype.update = function(req, res){
+    var data;
+    if (!(data = this.processBody(req, res))) {
+      return;
+    }
+    if (this.PROTECT_GRAPHS && _(this.PROTECTED_GRAPH_IDS).contains(data.id)) {
+      return res.send({
+        result: "error",
+        message: "Graph '" + data.id + "' is read-only."
+      }, 403);
+    }
+    return fs.writeFile(this.toFile(data.id), JSON.stringify(data), "utf8", this.errorHandler(res, "Error writing graph!"));
+  };
+  prototype.destroy = function(req, res){
+    var id;
+    id = req.param.graph;
+    if (this.PROTECT_GRAPHS && _(this.PROTECTED_GRAPH_IDS).contains(id)) {
+      return res.send({
+        result: "error",
+        message: "Graph '" + id + "' is read-only."
+      }, 403);
+    }
+    return fs.unlink(this.toFile(id), this.errorHandler(res, "Graph '" + id + "' does not exist!"));
+  };
+  prototype.processBody = function(req, res){
+    var data;
+    if (!req.body) {
+      res.send({
+        result: "error",
+        message: "Data required!"
+      }, 501);
+      return false;
+    }
+    data = req.body;
+    data.slug || (data.slug = data.id);
+    data.id || (data.id = data.slug);
+    if (!data.slug) {
+      res.send({
+        result: "error",
+        message: "Slug required!"
+      }, 501);
+      return false;
+    }
+    if (!exists(this.dataDir)) {
+      mkdirp(this.dataDir);
+    }
+    return data;
+  };
+  prototype.errorHandler = function(res, msg){
+    return function(err){
+      var msg;
+      if (err) {
+        msg || (msg = err.message || String(err));
+        console.error(msg);
+        return res.send({
+          result: "error",
+          message: msg
+        }, 501);
+      } else {
+        return res.send({
+          result: "ok"
+        });
+      }
+    };
+  };
+  return GraphController;
+}(Controller));
+module.exports = exports = GraphController;
+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/index.js b/lib/server/controllers/index.js
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/server/file-controller.js b/lib/server/file-controller.js
new file mode 100644 (file)
index 0000000..2704840
--- /dev/null
@@ -0,0 +1,112 @@
+var fs, path, exists, Seq, yaml, mkdirp, mkdirpAsync, readJSONFilesAsync, Controller, FileBackedController, 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;
+Controller = require('./controller');
+/**
+ * @class Resource controller backed by flat json or yaml files.
+ */
+FileBackedController = (function(superclass){
+  FileBackedController.displayName = 'FileBackedController';
+  var prototype = __extend(FileBackedController, superclass).prototype, constructor = FileBackedController;
+  prototype.name = null;
+  prototype.dataDir = null;
+  function FileBackedController(){
+    this.dataDir || (this.dataDir = "data/" + this.name);
+    superclass.apply(this, arguments);
+  }
+  /**
+   * Override to customize lookup of files by ID.
+   * 
+   * @param {String} id ID of this resource.
+   * @returns {String} Path to file for this resource.
+   */
+  prototype.toFile = function(id){
+    return this.dataDir + "/" + id + ".json";
+  };
+  /**
+   * Auto-load :id for related requests.
+   * 
+   * @param {String} id ID of the resource.
+   * @param {Function} cb Callback to invoke with the loaded object.
+   */
+  prototype.autoload = function(id, cb){
+    var file, parser, yamlFile;
+    file = this.toFile(id);
+    parser = JSON.parse;
+    yamlFile = file.replace(/\.json$/i, '.yaml');
+    if (exists(yamlFile)) {
+      file = yamlFile;
+      parser = yaml.load;
+    }
+    return fs.readFile(file, 'utf8', function(err, data){
+      if ('ENOENT' === (err != null ? err.code : void 8)) {
+        return cb(null, {});
+      }
+      if (err) {
+        console.error(this + ".autoload(" + id + ", " + typeof cb + ") -->\nerr");
+        return cb(err);
+      }
+      try {
+        return cb(null, parser(data));
+      } catch (err) {
+        console.error(this + ".autoload(" + id + ", " + typeof cb + ") -->\nerr");
+        return cb(err);
+      }
+    });
+  };
+  prototype.processBody = function(req, res){
+    var data;
+    if (!req.body) {
+      res.send({
+        result: "error",
+        message: "Data required!"
+      }, 501);
+      return false;
+    }
+    data = req.body;
+    data.slug || (data.slug = data.id);
+    data.id || (data.id = data.slug);
+    if (!data.slug) {
+      res.send({
+        result: "error",
+        message: "Slug required!"
+      }, 501);
+      return false;
+    }
+    if (!exists(this.dataDir)) {
+      mkdirp(this.dataDir);
+    }
+    return data;
+  };
+  prototype.errorHandler = function(res, msg){
+    return function(err){
+      var msg;
+      if (err) {
+        msg || (msg = err.message || String(err));
+        console.error(msg);
+        return res.send({
+          result: "error",
+          message: msg
+        }, 501);
+      } else {
+        return res.send({
+          result: "ok"
+        });
+      }
+    };
+  };
+  return FileBackedController;
+}(Controller));
+module.exports = exports = FileBackedController;
+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/files.js b/lib/server/files.js
new file mode 100644 (file)
index 0000000..f6b2cb5
--- /dev/null
@@ -0,0 +1,111 @@
+/**
+ * @fileOverview Filesystem utilities.
+ */
+var fs, path, Seq, glob, readFilesAsync, readJSONFilesAsync, logErrorsAnd, files, u, paths, _, __slice = [].slice;
+fs = require('fs');
+path = require('path');
+_ = require('underscore');
+Seq = require('seq');
+glob = require('glob');
+/**
+ * Asynchronously reads the text for each filepath produced by the
+ * globs supplied, returning a map from filepath to contents.
+ * 
+ * @param {String|Array<String>} patterns List of file-paths and/or glob-patterns to read.
+ * @param {Function} cb Callback taking `(error, data)` where `data` is a map
+ *  from filepath to contents. As always, `error` will be null on success.
+ * @returns {Seq} The Seq object representing the async operation chain. (You
+ *  can usually ignore this.)
+ */
+readFilesAsync = exports.readFilesAsync = function(patterns, cb){
+  var files, data;
+  if (typeof patterns === 'string') {
+    patterns = [patterns];
+  }
+  files = [];
+  data = {};
+  return Seq(patterns).parMap(function(pat){
+    return glob(pat, {
+      nocase: true,
+      nosort: true
+    }, this);
+  }).flatten().parMap(function(f){
+    files.push(f);
+    return fs.readFile(f, 'utf8', this);
+  }).parEach(function(text, i){
+    var f;
+    f = files[i];
+    data[f] = text;
+    return this.ok();
+  }).seq(function(){
+    return cb(null, data);
+  })['catch'](function(err){
+    console.error(err.file, err);
+    return cb(err);
+  });
+};
+/**
+ * Asynchronously reads text and parses JSON for each filepath produced by the
+ * globs supplied, returning a map from filepath to contents.
+ * 
+ * @param {String|Array<String>} patterns List of filepaths and/or glob-patterns to read.
+ * @param {Function} cb Callback taking `(error, data)` where `data` is a map
+ *  from filepath to contents. As always, `error` will be null on success.
+ * @returns {Seq} The Seq object representing the async operation chain. (You
+ *  can usually ignore this.)
+ */
+readJSONFilesAsync = exports.readJSONFilesAsync = function(patterns, cb){
+  var data;
+  data = {};
+  return Seq().seq(function(){
+    return readFilesAsync(patterns, this);
+  }).seq(function(data){
+    return this.ok(_.map(data, function(text, f){
+      return [f, text];
+    }));
+  }).flatten(false).parMap(function(_arg){
+    var f, text;
+    f = _arg[0], text = _arg[1];
+    try {
+      data[f] = JSON.parse(text);
+      return this.ok();
+    } catch (err) {
+      err.file = f;
+      console.error(f, err);
+      return cb(err);
+    }
+  }).seq(function(){
+    return cb(null, data);
+  })['catch'](function(err){
+    console.error(err.file, err);
+    return cb(err);
+  });
+};
+logErrorsAnd = exports.logErrorsAnd = function(cb){
+  return function(err){
+    var args;
+    args = __slice.call(arguments, 1);
+    global.args = arguments;
+    if (err) {
+      return console.error(err);
+    } else {
+      if (cb) {
+        return cb.apply(null, args);
+      }
+    }
+  };
+};
+if (require.main === module) {
+  files = exports;
+  u = require('kraken/util/underscore');
+  paths = ['package.*', 'deploy.sh'];
+  files.readFilesAsync(paths, function(err, data){
+    if (err) {
+      return console.error(err);
+    } else {
+      return console.log('\n\n', global.data = u.map(data, function(txt, f){
+        return f + ": " + txt.length;
+      }));
+    }
+  });
+}
\ No newline at end of file
diff --git a/lib/server/middleware.js b/lib/server/middleware.js
new file mode 100644 (file)
index 0000000..dcb72af
--- /dev/null
@@ -0,0 +1,263 @@
+var exists, fs, dirname, path, exec, spawn, subproc, glob, yaml, Seq, express, op, mkdirp, mkdirpAsync, readFilesAsync, Controller, BASE, DATA, LIB, SRC, STATIC, VAR, WWW, NODE_ENV, IS_DEV, IS_PROD, LOG_LEVEL, REV, DEFAULT_OPTIONS, exports, application, _ref, _;
+fs = (_ref = require('fs'), exists = _ref.existsSync, _ref);
+path = (_ref = require('path'), dirname = _ref.dirname, _ref);
+subproc = (_ref = require('child_process'), exec = _ref.exec, spawn = _ref.spawn, _ref);
+glob = require('glob');
+yaml = require('js-yaml');
+Seq = require('seq');
+express = require('express');
+_ref = require('../util'), _ = _ref._, op = _ref.op;
+_ref = require('./mkdirp'), mkdirp = _ref.mkdirp, mkdirpAsync = _ref.mkdirpAsync;
+readFilesAsync = require('./files').readFilesAsync;
+Controller = require('./controller');
+/**
+ * Limn project-internals
+ */
+BASE = dirname(dirname(__dirname));
+DATA = BASE + "/data";
+LIB = BASE + "/llb";
+SRC = BASE + "/src";
+STATIC = BASE + "/static";
+VAR = BASE + "/var";
+WWW = BASE + "/www";
+NODE_ENV = process.env.NODE_ENV || 'development';
+IS_DEV = NODE_ENV === 'development';
+IS_PROD = NODE_ENV === 'production';
+LOG_LEVEL = process.env.KRAKEN_LOG_LEVEL;
+LOG_LEVEL || (LOG_LEVEL = IS_DEV ? 'INFO' : 'WARN');
+LOG_LEVEL = LOG_LEVEL.toUpperCase();
+REV = process.env.KRAKEN_REV || 'HEAD';
+try {
+  REV = require('../version');
+} catch (e) {}
+DEFAULT_OPTIONS = {
+  dataDir: './data',
+  proxy: {
+    enabled: false,
+    whitelist: null,
+    blacklist: null
+  }
+};
+exports = module.exports = limn;
+/**
+ * Create a new instance of the Limn middleware.
+ * @param {Object} [options={}] Options:
+ */
+function limn(options){
+  var app;
+  app = express.createServer();
+  app = _.extend(app, application);
+  app.init();
+  return app;
+}
+application = limn.application = {
+  /**
+   * @constructor
+   */
+  init: function(opts){
+    var opx, YAML_EXT_PAT, proxy;
+    opts == null && (opts = {});
+    this.REV = REV;
+    this.BASE = BASE;
+    opts = _.merge({}, DEFAULT_OPTIONS, opts);
+    opx = opts.proxy;
+    if (opx.enabled === false && (opx.whitelist || opx.blacklist)) {
+      opx.enabled = true;
+    }
+    if (opx.enabled) {
+      opx.whitelist || (opx.whitelist = [/.*/]);
+      opx.blacklist || (opx.blacklist = []);
+    }
+    this.set('limn options', opts);
+    this.configure(function(){
+      this.set('views', WWW);
+      this.set('view engine', 'jade');
+      this.set('view options', __import({
+        layout: false,
+        version: REV,
+        IS_DEV: IS_DEV,
+        IS_PROD: IS_PROD
+      }, require('./view-helpers')));
+      this.use(require('./reqinfo')({}));
+      this.use(express.bodyParser());
+      this.use(express.methodOverride());
+      this.use(this.router);
+      return this.use(require('browserify')({
+        mount: '/vendor/browserify.js',
+        require: ['seq', 'd3', 'events'],
+        cache: BASE + "/.cache/browserify/cache.json"
+      }));
+    });
+    this.configure('production', function(){
+      this.use(express.logger());
+      this.set('static max age', 108000000);
+      return this.use(express.errorHandler());
+    });
+    this.configure('development', function(){
+      var compiler;
+      this.use(express.errorHandler({
+        dumpExceptions: true,
+        showStack: true
+      }));
+      this.set('view options').pretty = true;
+      compiler = require('connect-compiler-extras');
+      this.use('/js/kraken', compiler({
+        enabled: 'coco',
+        src: SRC,
+        dest: VAR + "/js/kraken",
+        log_level: LOG_LEVEL
+      }));
+      this.use(compiler({
+        enabled: ['jade-browser', 'stylus', 'yaml'],
+        src: WWW,
+        dest: VAR,
+        options: {
+          stylus: {
+            nib: true,
+            include: WWW + "/css"
+          }
+        },
+        log_level: LOG_LEVEL
+      }));
+      this.use(compiler({
+        enabled: 'yaml',
+        src: DATA,
+        dest: VAR + "/data",
+        log_level: LOG_LEVEL
+      }));
+      this.use(compiler({
+        enabled: 'commonjs_define',
+        src: [STATIC],
+        dest: VAR,
+        options: {
+          commonjs: {
+            drop_path_parts: 1,
+            drop_full_ext: false
+          },
+          commonjs_define: {
+            drop_path_parts: 1,
+            drop_full_ext: false
+          }
+        },
+        log_level: LOG_LEVEL
+      }));
+      return this.use(compiler({
+        enabled: 'commonjs_define',
+        src: [VAR, WWW],
+        dest: VAR,
+        options: {
+          commonjs: {
+            drop_path_parts: 1,
+            drop_full_ext: true
+          },
+          commonjs_define: {
+            drop_path_parts: 1,
+            drop_full_ext: true
+          }
+        },
+        log_level: LOG_LEVEL
+      }));
+    });
+    this.configure(function(){
+      var opts;
+      opts = this.set('static file options') || {};
+      this.use(express['static'](VAR, __clone(opts)));
+      this.use(express['static'](WWW, __clone(opts)));
+      return this.use(express['static'](STATIC, __clone(opts)));
+    });
+    this.controller(require('./controllers/graph'));
+    this.controller(require('./controllers/dashboard'));
+    YAML_EXT_PAT = /\.ya?ml$/i;
+    this.get('/datasources/all', function(req, res, next){
+      var data;
+      data = {};
+      return Seq().seq(glob, 'data/datasources/**/*.@(yaml|json)', {
+        nocase: true,
+        nosort: true
+      }, Seq).seq(function(paths){
+        return readFilesAsync(paths, this);
+      }).seq(function(txts){
+        return this.ok(_.items(txts));
+      }).flatten(false).parMap(function(_arg){
+        var f, text, k, v, that;
+        f = _arg[0], text = _arg[1];
+        k = f.replace(YAML_EXT_PAT, '.json');
+        v = data[k] = {};
+        try {
+          if (YAML_EXT_PAT.test(f)) {
+            v = data[k] = yaml.load(text);
+          } else {
+            v = data[k] = JSON.parse(text);
+          }
+          return this.ok(v);
+        } catch (err) {
+          console.error("[/data/all] catch! " + err);
+          console.error(err);
+          if (that = err.stack) {
+            console.error(that);
+          }
+          return res.send({
+            error: String(err),
+            partial_data: data
+          });
+        }
+      }).seq(function(){
+        return res.send(data);
+      })['catch'](function(err){
+        var that;
+        console.error('[/data/all] catch!');
+        console.error(err);
+        if (that = err.stack) {
+          console.error(that);
+        }
+        return res.send({
+          error: String(err),
+          partial_data: data
+        });
+      });
+    });
+    this.controller(require('./controllers/datasource'));
+    if (opts.proxy.enabled) {
+      proxy = require('./proxy')({
+        blacklist: opts.proxy.blacklist,
+        whitelist: opts.proxy.whitelist
+      });
+      this.get('/x', proxy);
+      this.get('/x/*', proxy);
+    }
+    this.get('/', function(req, res){
+      return res.render('dashboard/view');
+    });
+    this.get('/geo', function(req, res){
+      return res.render('geo');
+    });
+    this.get('/:type/:action/?', function(req, res, next){
+      var type, action, _ref;
+      _ref = req.params, type = _ref.type, action = _ref.action;
+      if (exists(WWW + "/" + type + "/" + action + ".jade")) {
+        return res.render(type + "/" + action);
+      } else {
+        return next();
+      }
+    });
+    this.get('/:type/?', function(req, res, next){
+      var type;
+      type = req.params.type;
+      if (exists(WWW + "/" + type + ".jade")) {
+        return res.render(type + "");
+      } else {
+        return next();
+      }
+    });
+    return this;
+  }
+};
+function __import(obj, src){
+  var own = {}.hasOwnProperty;
+  for (var key in src) if (own.call(src, key)) obj[key] = src[key];
+  return obj;
+}
+function __clone(it){
+  function fun(){} fun.prototype = it;
+  return new fun;
+}
\ No newline at end of file
diff --git a/lib/server/mkdirp.js b/lib/server/mkdirp.js
new file mode 100644 (file)
index 0000000..9506ed6
--- /dev/null
@@ -0,0 +1,65 @@
+var fs, path, expand, mkdirpAsync, mkdirpSync, mkdirp, __slice = [].slice;
+fs = require('fs');
+path = require('path');
+expand = exports.expand = function(){
+  var parts, p, home;
+  parts = __slice.call(arguments);
+  p = path.normalize(path.join.apply(path, parts));
+  if (p.indexOf('~') === 0) {
+    home = process.env.HOME || process.env.HOMEPATH;
+    p = path.join(home, p.slice(1));
+  }
+  return path.resolve(p);
+};
+mkdirpAsync = exports.mkdirpAsync = (function(){
+  function mkdirpAsync(p, mode, cb){
+    var _ref;
+    mode == null && (mode = 493);
+    if (typeof mode === 'function') {
+      _ref = [mode, 493], cb = _ref[0], mode = _ref[1];
+    }
+    cb || (cb = function(){});
+    p = expand(p);
+    return path.exists(p, function(exists){
+      var ps, _p;
+      if (exists) {
+        return cb(null);
+      }
+      ps = p.split('/');
+      _p = ps.slice(0, -1).join('/');
+      return mkdirpAsync(_p, mode, function(err){
+        if ((err != null ? err.code : void 8) === 'EEXIST') {
+          return cb(null);
+        }
+        if (err) {
+          return cb(err);
+        }
+        return fs.mkdir(_p, function(err){
+          if ((err != null ? err.code : void 8) === 'EEXIST') {
+            return cb(null);
+          }
+          if (err) {
+            return cb(err);
+          }
+        });
+      });
+    });
+  }
+  return mkdirpAsync;
+}());
+mkdirp = exports.mkdirp = mkdirpSync = exports.mkdirpSync = function(p, mode){
+  var made_any, part, _p, _i, _ref, _len;
+  mode == null && (mode = 493);
+  made_any = false;
+  _p = '';
+  for (_i = 0, _len = (_ref = expand(p).slice(1).split('/')).length; _i < _len; ++_i) {
+    part = _ref[_i];
+    _p += '/' + part;
+    if (path.existsSync(_p)) {
+      continue;
+    }
+    made_any = true;
+    fs.mkdirSync(_p, mode);
+  }
+  return made_any;
+};
\ No newline at end of file
diff --git a/lib/server/proxy.js b/lib/server/proxy.js
new file mode 100644 (file)
index 0000000..5accd9a
--- /dev/null
@@ -0,0 +1,66 @@
+var url, minimatch, request, matchesList, ProxyMiddleware, exports, _;
+_ = require('underscore');
+url = require('url');
+minimatch = require('minimatch');
+request = require('request');
+matchesList = function(list, value){
+  var pat, _i, _len;
+  for (_i = 0, _len = list.length; _i < _len; ++_i) {
+    pat = list[_i];
+    if (pat(value)) {
+      return true;
+    }
+  }
+  return false;
+};
+ProxyMiddleware = function(options){
+  var whitelist, blacklist, _ref;
+  options == null && (options = {});
+  _ref = options = (__import({
+    whitelist: [],
+    blacklist: []
+  }, options)), whitelist = _ref.whitelist, blacklist = _ref.blacklist;
+  whitelist = whitelist.map(function(it){
+    return minimatch.filter(it, {
+      nocase: true
+    });
+  });
+  blacklist = blacklist.map(function(it){
+    return minimatch.filter(it, {
+      nocase: true
+    });
+  });
+  return function(req, res){
+    var targetUrl, target;
+    targetUrl = (req.params.url || url.parse(req.url).pathname.slice(3)).trim();
+    if (!targetUrl) {
+      return res.send({
+        error: 'URL required'
+      }, 400);
+    }
+    if (!/^https?:\/\//.test(targetUrl)) {
+      targetUrl = "http://" + targetUrl;
+    }
+    target = url.parse(targetUrl, true, true);
+    if (matchesList(blacklist, target.hostname)) {
+      return res.send({
+        error: 'Domain is blacklisted'
+      }, 403);
+    }
+    if (!matchesList(whitelist, target.hostname)) {
+      return res.send({
+        error: 'Domain is not whitelisted'
+      }, 403);
+    }
+    res.header('X-Accel-Buffering', 'no');
+    console.log("[Proxy] " + targetUrl);
+    return request.get(targetUrl).pipe(res);
+  };
+};
+module.exports = exports = ProxyMiddleware;
+exports.ProxyMiddleware = ProxyMiddleware;
+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/reqinfo.js b/lib/server/reqinfo.js
new file mode 100644 (file)
index 0000000..b08cc89
--- /dev/null
@@ -0,0 +1,27 @@
+var url, exports, ReqInfoMiddleware, _;
+_ = require('underscore');
+url = require('url');
+module.exports = exports = function(options){
+  var mw;
+  mw = new ReqInfoMiddleware(options);
+  return mw.respond;
+};
+exports.ReqInfoMiddleware = ReqInfoMiddleware = (function(){
+  ReqInfoMiddleware.displayName = 'ReqInfoMiddleware';
+  var prototype = ReqInfoMiddleware.prototype, constructor = ReqInfoMiddleware;
+  function ReqInfoMiddleware(options){
+    this.options = options != null
+      ? options
+      : {};
+    _.bindAll(this, 'respond');
+  }
+  prototype.parse = url.parse;
+  prototype.respond = (function(){
+    function reqinfo(req, res, next){
+      req.info = this.parse(req.url);
+      return next();
+    }
+    return reqinfo;
+  }());
+  return ReqInfoMiddleware;
+}());
\ No newline at end of file
diff --git a/lib/server/server.js b/lib/server/server.js
new file mode 100644 (file)
index 0000000..79bf110
--- /dev/null
@@ -0,0 +1,62 @@
+var fs, path, exec, spawn, exists, express, LimnMiddleware, exports, app, limn, mainfile, PORT, that, NODE_ENV, REV, _ref;
+fs = require('fs');
+path = require('path');
+_ref = require('child_process'), exec = _ref.exec, spawn = _ref.spawn;
+exists = fs.existsSync || path.existsSync;
+express = require('express');
+LimnMiddleware = require('./middleware');
+app = exports = module.exports = express.createServer();
+/**
+ * Handle webhook notification to pull from origin.
+ */
+app.all('/webhook/post-update', function(req, res){
+  var cmd, child;
+  cmd = 'git pull origin master';
+  console.log("[/webhook/post-update] $ " + cmd);
+  return child = exec(cmd, function(err, stdout, stderr){
+    res.contentType('.txt');
+    console.log('[/webhook/post-update]  ', stdout);
+    console.log('[/webhook/post-update]  ', stderr);
+    if (err) {
+      console.error('[/webhook/post-update] ERROR!', err);
+      return res.send("$ " + cmd + "\n\n" + stdout + "\n\n" + stderr + "\n\nERROR! " + err, 503);
+    } else {
+      return res.send("$ " + cmd + "\n\n" + stdout + "\n\n" + stderr, 200);
+    }
+  });
+});
+/**
+ * Load Limn middleware
+ */
+app.use(limn = app.limn = LimnMiddleware());
+mainfile = path.basename((_ref = require.main) != null ? _ref.filename : void 8);
+if (require.main === module || 'Cokefile' === mainfile) {
+  PORT = 8081;
+  if (that = process.env.KRAKEN_PORT) {
+    PORT = parseInt(that, 10);
+  }
+  NODE_ENV = process.env.NODE_ENV || 'development';
+  REV = process.env.KRAKEN_REV || 'HEAD';
+  try {
+    REV = require('../version');
+  } catch (e) {}
+  exec('git rev-parse --short HEAD', {
+    cwd: process.cwd(),
+    env: process.env
+  }, function(err, stdout, stderr){
+    var REV, s;
+    if (err) {
+      throw err;
+    }
+    if (!REV) {
+      REV = stdout.trim();
+    }
+    console.log(s = "Kraken Server (port=" + PORT + ", env=" + NODE_ENV + ", rev=" + REV + ", base_dir=" + limn.BASE + ")");
+    console.log(__repeatString('=', s.length));
+    return app.listen(PORT);
+  });
+}
+function __repeatString(str, n){
+  for (var r = ''; n > 0; (n >>= 1) && (str += str)) if (n & 1) r += str;
+  return r;
+}
\ No newline at end of file
diff --git a/lib/server/view-helpers.js b/lib/server/view-helpers.js
new file mode 100644 (file)
index 0000000..ed94c9a
--- /dev/null
@@ -0,0 +1,57 @@
+var CWD, WWW, VAR, STATIC, VERSION, fs, path, yaml, jade, NODE_ENV, IS_PROD, IS_TEST, IS_DEV, SOURCES_ENV, sources, joinTree, _;
+CWD = exports.CWD = process.cwd();
+WWW = exports.WWW = CWD + "/www";
+VAR = exports.VAR = CWD + "/var";
+STATIC = exports.STATIC = CWD + "/static";
+VERSION = 'HEAD';
+try {
+  VERSION = require('../version');
+} catch (e) {}
+exports.VERSION = exports.version = VERSION;
+fs = exports.fs = require('fs');
+path = exports.path = require('path');
+_ = exports._ = require('underscore');
+_.str = require('underscore.string');
+_.mixin(_.str.exports());
+yaml = exports.yaml = require('js-yaml');
+jade = exports.jade = require('jade');
+exports.env = process.env;
+NODE_ENV = exports.NODE_ENV = (process.env.NODE_ENV || 'development').toLowerCase();
+IS_PROD = exports.IS_PROD = NODE_ENV === 'production';
+IS_TEST = exports.IS_TEST = NODE_ENV === 'test';
+IS_DEV = exports.IS_DEV = !(IS_PROD || IS_TEST);
+SOURCES_ENV = process.env.KRAKEN_FORCE_BUNDLES ? 'production' : NODE_ENV;
+/**
+ * Reify a modules.yaml file
+ */
+sources = exports.sources = function(modulesFile, node_env){
+  var mods, modlist;
+  node_env == null && (node_env = SOURCES_ENV);
+  mods = yaml.load(fs.readFileSync(modulesFile, 'utf8'));
+  modlist = (mods.all || []).concat(mods[node_env] || []);
+  return _.flatten(modlist.map(function(_arg){
+    var suffix, paths;
+    suffix = _arg.suffix || '', paths = _arg.paths;
+    return joinTree('', paths).map(function(it){
+      return it + suffix;
+    });
+  }));
+};
+joinTree = exports.joinTree = (function(){
+  function joinTree(root, tree){
+    if (typeof tree === 'string') {
+      return [root + "/" + tree];
+    }
+    return _(tree).reduce(function(acc, branch){
+      if (typeof branch === 'string') {
+        acc.push(root + "/" + branch);
+      } else {
+        _.each(branch, function(v, k){
+          return acc.push.apply(acc, joinTree(root + "/" + k, v));
+        });
+      }
+      return acc;
+    }, []);
+  }
+  return joinTree;
+}());
\ No newline at end of file
diff --git a/lib/util/aliasdict.js b/lib/util/aliasdict.js
new file mode 100644 (file)
index 0000000..0366b19
--- /dev/null
@@ -0,0 +1,175 @@
+var AliasDict, exports, _, __slice = [].slice;
+_ = require('kraken/util/underscore');
+/**
+ * @class A mapping of key-value pairs supporting key-aliases.
+ */
+AliasDict = (function(){
+  AliasDict.displayName = 'AliasDict';
+  var prototype = AliasDict.prototype, constructor = AliasDict;
+  /**
+   * Data store.
+   * @type Object
+   * @private
+   */
+  prototype._data = null;
+  /**
+   * Mapping from keys to an array of [potentially nested] alias-keys.
+   * @type Object<String, Array<String>>
+   * @private
+   */
+  prototype._aliases = null;
+  /**
+   * @constructor
+   */;
+  function AliasDict(){
+    this._data = {};
+    this._aliases = {};
+    this.extend.apply(this, arguments);
+  }
+  /**
+   * @returns {Number} Number of real keys in the Dict.
+   */
+  prototype.size = function(){
+    return _.keys(this._data).length;
+  };
+  /**
+   * @returns {AliasDict} A copy of the AliasDict, including aliases as well as data.
+   */
+  prototype.clone = function(){
+    var d;
+    d = new AliasDict(this._data);
+    _.each(this._aliases, function(v, k){
+      return d.setAlias(k, v.slice());
+    });
+    return d;
+  };
+  /**
+   * @returns {Boolean} Whether there is a value at the given key.
+   */
+  prototype.has = function(key){
+    return this.get(key) != null;
+  };
+  /**
+   * @returns {*} Ignores aliases, returning the value at key or `undefined`.
+   */
+  prototype.getValue = function(key){
+    var prop;
+    prop = _.getNested(this._data, key);
+    if (prop != null) {
+      return prop.value;
+    }
+  };
+  prototype.get = function(key, def){
+    var aliases, val;
+    aliases = this._aliases[key] || [key];
+    val = aliases.reduce(function(val, alias){
+      var prop;
+      if ((val != null) !== undefined) {
+        return val;
+      }
+      prop = _.getNested(this._data, alias);
+      if (prop != null) {
+        return prop.value;
+      }
+    }, undefined);
+    if (val !== undefined) {
+      return val;
+    } else {
+      return def;
+    }
+  };
+  prototype.set = function(key, val){
+    _.setNested(this._data, key, val, {
+      ensure: true
+    });
+    return val;
+  };
+  prototype.del = function(key){
+    var prop;
+    prop = _.getNestedMeta(key);
+    if (prop) {
+      delete prop.obj[prop.key];
+      return prop.value;
+    }
+  };
+  prototype.hasAlias = function(key){
+    return this._aliases[key] != null;
+  };
+  prototype.getAlias = function(key, def){
+    def == null && (def = []);
+    return this._aliases[key] || def;
+  };
+  prototype.setAlias = function(key, aliases){
+    this._aliases[key] = _.isArray(aliases)
+      ? aliases
+      : [aliases];
+    return this;
+  };
+  prototype.addAlias = function(key){
+    var aliases;
+    aliases = __slice.call(arguments, 1);
+    this._aliases[key] = _.flatten(this.getAlias(key, [key]).concat(aliases));
+    return this;
+  };
+  prototype.delAlias = function(key){
+    var _ref, _ref2;
+    return _ref2 = (_ref = this._aliases)[key], delete _ref[key], _ref2;
+  };
+  prototype.toObject = function(){
+    return _.clone(this._data);
+  };
+  prototype.keys = function(){
+    return _.keys(this._data);
+  };
+  prototype.values = function(){
+    return _.values(this._data);
+  };
+  prototype.extend = function(){
+    var args, o, k, v, _i, _len;
+    args = __slice.call(arguments);
+    for (_i = 0, _len = args.length; _i < _len; ++_i) {
+      o = args[_i];
+      for (k in o) {
+        v = o[k];
+        this.set(k, v);
+      }
+    }
+    return this;
+  };
+  prototype.reduce = function(fn, acc, context){
+    context == null && (context = this);
+    return _.reduce(this._data, fn, acc, context);
+  };
+  prototype.map = function(fn, context){
+    context == null && (context = this);
+    return _.map(this._data, fn, context);
+  };
+  prototype.filter = function(fn, context){
+    context == null && (context = this);
+    return _.filter(this._data, fn, context);
+  };
+  prototype.each = function(fn, context){
+    context == null && (context = this);
+    _.each(this._data, fn, context);
+    return this;
+  };
+  prototype.invoke = function(name){
+    var args;
+    args = __slice.call(arguments, 1);
+    return _.invoke.apply(_, [this._data, name].concat(__slice.call(args)));
+  };
+  prototype.pluck = function(attr){
+    return _.pluck(this._data, attr);
+  };
+  prototype.find = function(fn, context){
+    context == null && (context = this);
+    return _.find(this._data, fn, context);
+  };
+  prototype.toString = function(){
+    var Cls;
+    Cls = this.constructor;
+    return (Cls.displayName || Cls.name) + "()";
+  };
+  return AliasDict;
+}());
+module.exports = exports = AliasDict;
\ No newline at end of file
diff --git a/lib/util/bitstring.js b/lib/util/bitstring.js
new file mode 100644 (file)
index 0000000..3a0511d
--- /dev/null
@@ -0,0 +1,244 @@
+var SEEK_ABSOLUTE, SEEK_RELATIVE, SEEK_FROM_EOF, bin, binlen, mask, chr, ord, BitString, exports;
+SEEK_ABSOLUTE = 0;
+SEEK_RELATIVE = 1;
+SEEK_FROM_EOF = 2;
+bin = function(n){
+  var s;
+  do {
+    s = (n % 2 ? '1' : '0') + (s || '');
+    n >>= 1;
+  } while (n);
+  return s;
+};
+binlen = function(n){
+  return bin(Math.abs(n)).length;
+};
+mask = function(n){
+  return (1 << n) - 1;
+};
+chr = function(it){
+  return String.fromCharCode(it);
+};
+ord = function(it){
+  return String(it).charCodeAt(0);
+};
+/**
+ * File-like object for reading/writing bits.
+ * @class
+ */
+BitString = (function(){
+  BitString.displayName = 'BitString';
+  var prototype = BitString.prototype, constructor = BitString;
+  prototype.buf = null;
+  prototype._pos = -1;
+  prototype._spill = 0;
+  prototype._spillen = 0;
+  prototype._peek = 0;
+  prototype._peeklen = 0;
+  function BitString(source, buf){
+    var i, _to;
+    source == null && (source = '');
+    buf == null && (buf = []);
+    this.buf = buf.slice();
+    for (i = 0, _to = source.length; i < _to; ++i) {
+      this._bufwrite(source.charCodeAt(i));
+    }
+  }
+  prototype.size = function(){
+    return this.buf.length + (this._spillen ? 1 : 0);
+  };
+  prototype.bitsize = function(){
+    return this.buf.length * 8 + this._spillen;
+  };
+  prototype._bufwrite = function(b){
+    if (this._pos === -1) {
+      this.buf.push(b);
+    } else {
+      this.buf[this._pos] = b;
+      if (++this._pos >= this.buf.length) {
+        this._pos = -1;
+      }
+    }
+    return this;
+  };
+  prototype.writebits = function(n, size){
+    var bits, b;
+    size = size || binlen(n);
+    bits = this._spill << size | n;
+    size += this._spillen;
+    while (size >= 8) {
+      size -= 8;
+      b = bits >> size;
+      bits &= mask(size);
+      this._bufwrite(b);
+    }
+    this._spill = bits;
+    this._spillen = size;
+    return this;
+  };
+  prototype.flush = function(){
+    var b;
+    b = this._spill;
+    if (this._spillen) {
+      b <<= 8 - this._spillen;
+      this._bufwrite(b);
+    }
+    this._spill = 0;
+    this._spillen = 0;
+    return this;
+  };
+  prototype.truncate = function(){
+    this.buf = [];
+    this._pos = -1;
+&n