From: dsc Date: Sun, 1 Apr 2012 08:24:55 +0000 (-0700) Subject: Converts the hodgepodge of server endpoints to RESTful resources. X-Git-Url: http://git.less.ly:3516/?a=commitdiff_plain;h=188ef6c8e5a22d6f0ad0be0c2f980824169f4574;p=limn.git Converts the hodgepodge of server endpoints to RESTful resources. --- diff --git a/lib/server/controller.co b/lib/server/controller.co new file mode 100644 index 0000000..15b6885 --- /dev/null +++ b/lib/server/controller.co @@ -0,0 +1,94 @@ +express = require 'express' +Resource = require 'express-resource' + +hasOwn = Object::hasOwnProperty + +/** + * @class Resource controller for easily subclassing an express Resource. + */ +class Controller extends Resource + /** + * Singular, lowercase resource-noun. + * @optional + * @type String + * @example "user" + */ + id : null + + /** + * Plural, lowercase resource-noun. + * @required + * @type String + * @example "users" + */ + name : null + + /** + * Resource routing prefix. + * @optional + * @type String + */ + base : '/' + + /** + * Default format. + * @type String + */ + 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. + * @type Object + */ + mapping : null + + + + /** + * @constructor + */ + (@app, name) -> + + # Bind all methods and make actions object + actions = {} + for k, fn in this + continue unless typeof fn is 'function' and k is not 'constructor' + actions[k] = this[k] = fn.bind this + + # Replace/remove .load, as by default it's Resource#load + delete actions.load + if typeof actions.autoload is 'function' + actions.load = actions.autoload + + super (name or @name), actions, @app + (@app.resources or= {})[@name] = this + @applyControllerMapping() + + + /** + * Apply the contents of a mapping hash. + * @private + */ + applyControllerMapping: (mapping=@mapping) -> + for subroute, methods in mapping + if typeof methods is 'string' + methods = { all:methods } + for verb, method in methods + @map verb, subroute, @[method] + this + + toString: -> + "#{@.constructor.name}('#{@name}', base='#{@base}', app=#{@app})" + + +express.HTTPServer::controller = \ +express.HTTPSServer::controller = (name, ControllerClass, opts) -> + [opts, ControllerClass, name] = [ControllerClass, name, null] if typeof name is 'function' + new ControllerClass this, name + + +module.exports = exports = Controller diff --git a/lib/server/controllers/dashboard.co b/lib/server/controllers/dashboard.co new file mode 100644 index 0000000..e69de29 diff --git a/lib/server/controllers/datasource.co b/lib/server/controllers/datasource.co new file mode 100644 index 0000000..90c68f3 --- /dev/null +++ b/lib/server/controllers/datasource.co @@ -0,0 +1,74 @@ +fs = require 'fs' +Seq = require 'seq' +Controller = require '../controller' + +YAML_EXT_PAT = /\.ya?ml$/i + + + +/** + * @class Resource controller for graph requests. + */ +class DataSourceController extends Controller + name : 'datasources' + dataDir : 'data/graphs' + + mapping : + all : 'getAllData' + + -> super ... + + /** + * Returns a JSON listing of the datasource metadata files. + */ + index : (req, res, next) -> + fs.readdir @dataDir, (err, files) -> + res.send do + files.filter -> /\.(json|ya?ml)$/i.test it + .map -> "/#{@dataDir}#it".replace YAML_EXT_PAT, '.json' + + /** + * Returns the aggregated JSON content of the datasource metadata files. + */ + allData : (req, res, next) -> + data = {} + files = [] + Seq() + .seq fs.readdir, @dataDir, Seq + .flatten() + .filter -> /\.(json|ya?ml)$/.test it + .seq -> + files := @stack.slice() + # console.log 'files:', files + @ok files + .flatten() + .parMap_ (next, f) ~> + # console.log "fs.readFile '#CWD/data/#f'" + fs.readFile "#{@dataDir}/#f", 'utf8', next + .parMap (text, i) -> + f = files[i] + # console.log "parsing file[#i]: '#f' -> text[#{text.length}]..." + 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 + # console.log "#f ok!", data + @ok v + catch err + console.error "[/data/all] catch! #err" + console.error err + console.error that if err.stack + res.send { error:String(err), partial_data:data } + .seq -> res.send data + .catch (err) -> + console.error '[/data/all] catch!' + console.error err + console.error that if err.stack + res.send { error:String(err), partial_data:data } + + + +module.exports = exports = DataSourceController diff --git a/lib/server/controllers/graph.co b/lib/server/controllers/graph.co new file mode 100644 index 0000000..8017081 --- /dev/null +++ b/lib/server/controllers/graph.co @@ -0,0 +1,107 @@ +_ = require 'underscore' +fs = require 'fs' +path = require 'path' +yaml = require 'js-yaml' + +{existsSync:exists} = path +{mkdirp, mkdirpAsync} = require '../mkdirp' +Controller = require '../controller' + + +/** + * @class Resource controller for graph requests. + */ +class GraphController extends Controller + name : 'graphs' + dataDir : 'data/graphs' + -> super ... + + + toFile: (id) -> "#{@dataDir}/#id.json" + + /** + * Auto-load :id for related requests. + */ + autoload: (id, cb) -> + file = @toFile id + parser = JSON.parse + + yamlFile = file.replace /\.json$/i, '.yaml' + if exists yamlFile + file = yamlFile + parser = yaml.load + + err, data <- fs.readFile file, 'utf8' + if err + console.error "GraphController.autoload(#id, #{typeof cb}) -->\nerr" + return cb err + try + cb null, parser data + catch err + console.error "GraphController.autoload(#id, #{typeof cb}) -->\nerr" + cb err + + # GET /graphs + index: (req, res) -> + res.render 'dashboard' + + # GET /graphs/:graph + show: (req, res) -> + res.send req.graph + + # GET /graphs/:graph/edit + edit: (req, res) -> + res.send req.graph + + # GET /graphs/new + new: (req, res) -> + ... + + # POST /graphs + create: (req, res) -> + return unless data = @processBody req, res + file = @toFile data.id + if exists file + return res.send { result:"error", message:"Graph already exists!" } + else + fs.writeFile file, JSON.stringify(data), "utf8", @errorHandler(res, "Error writing graph!") + + # PUT /graphs/:graph + update: (req, res) -> + return unless data = @processBody req, res + fs.writeFile @toFile(data.id), JSON.stringify(data), "utf8", @errorHandler(res, "Error writing graph!") + + # DELETE /graphs/:graph + destroy: (req, res) -> + fs.unlink @toFile(req.param.graph), @errorHandler(res, "Graph does not exist!") + + + ### Helpers + + processBody: (req, res) -> + if not req.body + res.send {result:"error", message:"Data required!"}, 501 + return false + + data = req.body + data.slug or= data.id + data.id or= data.slug + + if not data.slug + res.send {result:"error", message:"Slug required!"}, 501 + return false + + mkdirp @dataDir if not exists @dataDir + return data + + errorHandler: (res, msg) -> + (err) -> + if err + msg or= err.message or String(err) + console.error msg + res.send { result:"error", message:msg }, 501 + else + res.send { result:"ok" } + + +module.exports = exports = GraphController diff --git a/lib/server/server.co b/lib/server/server.co index cec9568..fcf1090 100755 --- a/lib/server/server.co +++ b/lib/server/server.co @@ -2,7 +2,6 @@ fs = require 'fs' path = require 'path' -{parse} = require 'url' {existsSync:exists} = path {exec, spawn} = require 'child_process' {mkdirp, mkdirpAsync} = require './mkdirp' @@ -15,10 +14,10 @@ Seq = require 'seq' yaml = require 'js-yaml' mime = require 'mime' express = require 'express' +Resource = require 'express-resource' compiler = require 'connect-compiler-extras' - ### Config # TODO: read KRAKEN_PORT from ENV @@ -29,9 +28,11 @@ WWW = "#CWD/www" VAR = "#CWD/var" STATIC = "#CWD/static" DIST = "#CWD/dist" +DATA = "#CWD/data" NODE_ENV = process.env.NODE_ENV or 'dev' LOG_LEVEL = if _.startsWith NODE_ENV, 'dev' then 'INFO' else 'WARN' +# LOG_LEVEL = 'DEBUG' VERSION = 'dev' @@ -66,6 +67,7 @@ app.configure -> } import require './view-helpers' app.use express.logger() if LOG_LEVEL is 'DEBUG' + # app.use express.logger() # Parse URL, fiddle app.use require('./reqinfo')({}) @@ -87,6 +89,12 @@ app.configure -> options : stylus : { nib:true, include:"#WWW/css" } log_level : LOG_LEVEL + app.use compiler do + enabled : 'yaml' + src : DATA + dest : "#VAR/data" + log_level : LOG_LEVEL + # wrap modules in commonjs closure for browser app.use compiler do enabled : 'commonjs_define' @@ -113,7 +121,7 @@ app.configure -> app.use express.static VAR app.use express.static STATIC # app.use express.static DIST if exists DIST - app.use express.static DIST + app.use express.static DIST if NODE_ENV is 'prod' # Serve directory listings # app.use express.directory WWW @@ -126,58 +134,15 @@ app.configure -> showStack : true -/* * * * Routes * * * {{{ */ -app.get '/', (req, res) -> - res.render 'dashboard' +/* * * * Routes and Controllers * * * {{{ */ -saveGraph = (req, res, next) -> - if not req.body - return res.send {result:"error", message:"JSON required!"}, 501 - - data = req.body - {id, slug} = data - if not slug - return res.send {result:"error", message:"slug required!"}, 501 - mkdirp "#VAR/presets" if not exists "#VAR/presets" - - id or= slug - data.id = id - err <- fs.writeFile "#VAR/presets/#id.json", JSON.stringify(data), "utf8" - if err - res.send { result:"error", message:err.message or String(err) }, 501 - else - res.send { result:"ok" } +Controller = require './controller' +app.controller require './controllers/graph' +app.controller require './controllers/datasource' - -app.post '/graph/save', saveGraph -app.put '/graph/:slug\.json', saveGraph - -app.get '/graph/:slug\.json', (req, res, next) -> - req.url .= replace /^\/graph\//i, '/presets/' - next() - -app.get '/preset/:slug', (req, res, next) -> - {slug} = req.params - if exists("#VAR/presets/#slug.yaml") or exists("#VAR/presets/#slug.json") - req.url .= replace /^\/preset\/[^\/?]/i, "/presets/#slug.json" - # req.url += '.json' - next() - -app.get '/graph(/:slug)?/?', (req, res, next) -> - {slug} = req.params - # console.log '/graph/:slug/?' - # console.log ' slug: ', slug - # console.log ' params:', req.params - unless _.str.include slug, '.' - {pathname, search or ''} = req.info - req.url = path.join pathname, "view#search" - req.params.action = 'view' - next() - -app.get '/graph/:slug/:action/?', (req, res) -> - {slug, action} = req.params - res.render "graph/#action" +app.get '/', (req, res) -> + res.render 'dashboard' app.get '/:type/:action/?', (req, res, next) -> {type, action} = req.params @@ -186,85 +151,26 @@ app.get '/:type/:action/?', (req, res, next) -> else next() - - -# }}} -/* * * * Data Source Oracle * * * {{{ */ - -YAML_EXT_PAT = /\.ya?ml$/i - -/** - * Returns a JSON listing of the datasource metadata files. - */ -app.get '/data/list', (req, res, next) -> - fs.readdir "#CWD/data", (err, files) -> - res.send do - files.filter -> /\.(json|ya?ml)$/i.test it - .map -> "/data/#it".replace YAML_EXT_PAT, '.json' - -/** - * Returns the aggregated JSON content of the datasource metadata files. - */ -app.get '/data/all', (req, res, next) -> - data = {} - files = [] - Seq() - .seq fs.readdir, "#CWD/data", Seq - .flatten() - .filter -> /\.(json|ya?ml)$/.test it - .seq -> - files := @stack.slice() - # console.log 'files:', files - @ok files - .flatten() - .parMap (f) -> - # console.log "fs.readFile '#CWD/data/#f'" - fs.readFile "#CWD/data/#f", 'utf8', this - .parMap (text, i) -> - f = files[i] - # console.log "parsing file[#i]: '#f' -> text[#{text.length}]..." - 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 - # console.log "#f ok!", data - @ok v - catch err - console.error "[/data/all] catch! #err" - console.error err - console.error that if err.stack - res.send { error:String(err), partial_data:data } - .seq -> res.send data - .catch (err) -> - console.error '[/data/all] catch!' - console.error err - console.error that if err.stack - res.send { error:String(err), partial_data:data } - -# }}} - /** * Handle webhook notification to pull from origin. */ app.all '/webhook/post-update', (req, res) -> - + # exec the pull async... - console.log '[/webhook/post-update] $ git pull origin master' - child = exec 'git pull origin master', (err, stdout, stderr) -> + cmd = 'git pull origin master' + console.log "[/webhook/post-update] $ #cmd" + child = exec cmd, (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 - res.send "#stdout\n\n#stderr\n\nERROR! #err", 503 + res.send "$ #cmd\n\n#stdout\n\n#stderr\n\nERROR! #err", 503 else - res.send "#stdout\n\n#stderr", 200 - + res.send "$ #cmd\n\n#stdout\n\n#stderr", 200 +# }}} exports import { CWD, WWW, VAR, STATIC, diff --git a/package.co b/package.co index c66ff9e..b7698fb 100644 --- a/package.co +++ b/package.co @@ -12,6 +12,7 @@ dependencies : 'coco' : '>= 0.7.0' 'mime' : '>= 1.2.5' 'express' : '>= 2.5.8' + 'express-resource' : '>= 0.2.4' 'connect-compiler' : 'https://github.com/dsc/connect-compiler/tarball/master' 'connect-compiler-extras' : 'https://github.com/dsc/connect-compiler-extras/tarball/master' 'jade' : '>= 0.20.1' diff --git a/package.json b/package.json index 9c41e1a..838f439 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "coco": ">= 0.7.0", "mime": ">= 1.2.5", "express": ">= 2.5.8", + "express-resource": ">= 0.2.4", "connect-compiler": "https://github.com/dsc/connect-compiler/tarball/master", "connect-compiler-extras": "https://github.com/dsc/connect-compiler-extras/tarball/master", "jade": ">= 0.20.1",