{"action":"created","comment":{"url":"https://api.github.com/repos/koopjs/koop-agol/pulls/comments/35971879","id":35971879,"diff_hunk":"@@ -1,1019 +1,1054 @@\n-var request = require('request'),\n-  https = require('https'),\n-  sm = require('sphericalmercator'),\n-  merc = new sm({size:256}),\n-  crypto = require('crypto'),\n-  _ = require('lodash'),\n-  fs = require('fs');\n-\n-// inherit from base controller\n-var Controller = function( agol, BaseController ){\n-\n-  var controller = {};\n-  controller.__proto__ = BaseController( );\n-\n-  // Registers a host with the given id \n-  // this inserts a record into the db for an ArcGIS instances ie: id -> hostname :: arcgis -> arcgis.com \n-  controller.register = function(req, res){\n-    if ( !req.body.host ){\n-      res.status(400).send('Must provide a host to register'); \n-    } else { \n-      agol.register( req.body.id, req.body.host, function(err, id){\n-        if (err) {\n-          res.status(400).send( err );\n-        } else {\n-          res.json({ 'serviceId': id });\n-        }\n-      });\n+var https = require('https')\n+var Sm = require('sphericalmercator')\n+var merc = new Sm({size: 256})\n+var crypto = require('crypto')\n+var _ = require('lodash')\n+var fs = require('fs')\n+\n+var Controller = function (agol, BaseController) {\n+  /**\n+   * The primary controller onto which all methods are attached\n+   * @module Controller\n+   */\n+  var controller = BaseController()\n+\n+  /**\n+   * Registers a host with the given id\n+   * this inserts a record into the db for an ArcGIS instances ie: id -> hostname :: arcgis -> arcgis.com\n+   *\n+   * @param {object} req - the incoming request object\n+   * @param {object} res - the outgoing response object\n+   */\n+  controller.register = function (req, res) {\n+    if (!req.body.host) {\n+      return res.status(400).send('Must provide a host to register')\n     }\n-  };\n \n+    agol.register(req.body.id, req.body.host, function (err, id) {\n+      if (err) {\n+        return res.status(400).send(err)\n+      }\n+      res.json({ 'serviceId': id })\n+    })\n+  }\n \n-  // handles a DELETE to remove a registered host from the DB\n-  controller.del = function(req, res){\n-    if ( !req.params.id ){\n-      res.status(400).send( 'Must specify a service id');\n-      \n-    } else {\n-      agol.remove(req.params.id, function(err, data){\n-        if (err) {\n-          res.status(400).send( err );\n-        } else {\n-          res.json( data );\n-        }\n-      });\n+  /**\n+   * handles a DELETE to remove a registered host from the DB\n+   *\n+   * @param {object} req - the incoming request object\n+   * @param {object} res - the outgoing response object\n+   */\n+  controller.del = function (req, res) {\n+    if (!req.params.id) {\n+      return res.status(400).send('Must specify a service id')\n     }\n-  };\n \n+    agol.remove(req.params.id, function (err, data) {\n+      if (err) {\n+        return res.status(400).send(err)\n+      }\n+      res.json(data)\n+    })\n+  }\n \n   // returns a list of the registered hosts and thier ids\n-  controller.list = function(req, res){\n-    agol.find(null, function(err, data){\n+  /**\n+   *\n+   * @param {object} req - the incoming request object\n+   * @param {object} res - the outgoing response object\n+   */\n+  controller.list = function (req, res) {\n+    agol.find(null, function (err, data) {\n       if (err) {\n-        res.status(500).send( err );\n-      } else {\n-        res.json( data );\n+        return res.status(500).send(err)\n       }\n-    });\n-  };\n+      res.json(data)\n+    })\n+  }\n \n-  // looks up a host based on a given id \n-  controller.find = function(req, res){\n-    agol.find(req.params.id, function(err, data){\n+  /**\n+   * looks up a host based on a given id\n+   *\n+   * @param {object} req - the incoming request object\n+   * @param {object} res - the outgoing response object\n+   */\n+  controller.find = function (req, res) {\n+    agol.find(req.params.id, function (err, data) {\n       if (err) {\n-        res.status(err.code || 404).send( err );\n-      } else {\n-        res.json( data );\n+        return res.status(err.code || 404).send(err)\n       }\n-    });\n-  };\n+      res.json(data)\n+    })\n+  }\n \n-  // get the item metadata from the host \n-  controller.findItem = function(req, res){\n-    if (req.params.format){\n-      this.findItemData(req, res);\n-    } else {\n-      agol.find(req.params.id, function(err, data){\n-        if (err) {\n-          res.status(err.code || 404).send( err );\n-        } else {\n-          // Get the item \n-          agol.getItem( data.host, req.params.item, req.query, function(error, itemJson){\n-            if (error) {\n-              res.status(404).send( error);\n-            } else {\n-              res.contentType('text'); \n-              res.json( itemJson );\n-            }\n-          });\n-        }\n-      });\n+  /**\n+   * get the item metadata from the host\n+   *\n+   * @param {object} req - the incoming request object\n+   * @param {object} res - the outgoing response object\n+   */\n+  controller.findItem = function (req, res) {\n+    if (req.params.format) {\n+      return this.findItemData(req, res)\n     }\n-  };\n \n-  // drops the cache for an item\n-  controller.dropItem = function(req, res){\n-    // if we have a layer then append it to the query params \n-    if ( req.params.layer ) {\n-      req.query.layer = req.params.layer;\n+    agol.find(req.params.id, function (err, data) {\n+      if (err) {\n+        return res.status(err.code || 404).send(err)\n+      }\n+\n+      // Get the item\n+      agol.getItem(data.host, req.params.item, req.query, function (error, itemJson) {\n+        if (error) {\n+          return res.status(404).send(error)\n+        }\n+\n+        res.contentType('text')\n+        res.json(itemJson)\n+      })\n+    })\n+  }\n+\n+  /**\n+   * Drops the cache for an item\n+   * @param {object} req - the incoming request object\n+   * @param {object} res - the outgoing response object\n+   */\n+  controller.dropItem = function (req, res) {\n+    // if we have a layer then append it to the query params\n+    if (req.params.layer) {\n+      req.query.layer = req.params.layer\n     }\n \n-    agol.find(req.params.id, function(err, data){\n+    agol.find(req.params.id, function (err, data) {\n       if (err) {\n-        res.status(err.code || 404).send( err );\n-      } else {\n-        // Get the item \n-        agol.dropItem( data.host, req.params.item, req.query, function(error, itemJson){\n-          if (error) {\n-            res.status(error.code || 400).send( error );\n-          } else {\n-            res.json( itemJson );\n-          }\n-        });\n+        return res.status(err.code || 404).send(err)\n       }\n-    });\n-  };\n-\n-  // find the items data \n-  controller.findItemData = function(req, res){\n-    var self = this;\n-    // closure that actually goes out gets the data\n-    var _get = function(id, item, key, options, callback){\n-       agol.find( id, function( err, data ){\n-        if (err) {\n-          callback(err, null);\n-        } else {\n-          // Get the item\n-          if ( !parseInt(options.layer) ){\n-            options.layer = 0;\n-          }\n \n-          agol.getItemData( data.host, id, item, key, options, function(error, itemJson){\n-            if (error) {\n-              callback( error, null);\n-            // if we have status return right away\n-            } else if (itemJson.koop_status == 'processing' && typeof req.params.silent === 'undefined') {\n-              // return w/202\n-              agol.getCount(['agol', item, options.layer].join(':'), {}, function(err, count) {\n-                var code = 202;\n-                var response = {\n-                  status: 'processing',\n-                  processing_time: ( Date.now() - itemJson.retrieved_at)/1000 || 0, \n-                  count: count\n-                };\n-                if ( itemJson.generating ){\n-                  response.generating = itemJson.generating;\n-                  // we received an error from the server\n-                  if (itemJson.generating.error){\n-                    code = 502;\n-                  }\n-                }\n-                res.status( code ).json( response );\n-              }); \n-            } else {\n-              callback( null, itemJson );\n-            }\n-          });\n+      // Get the item\n+      agol.dropItem(data.host, req.params.item, req.query, function (error, itemJson) {\n+        if (error) {\n+          return res.status(error.code || 400).send(error)\n+        }\n+        res.json(itemJson)\n+      })\n+    })\n+  }\n+\n+  /**\n+   * Gets the actual json data from the model\n+   *\n+   * @param {object} params - required params used to fetch data\n+   * @param {object} options - optional query string based params\n+   * @param {function} callback\n+   */\n+  controller._getItemData = function (req, res, callback) {\n+    var params = req.params\n+    var options = req.query\n+    var id = params.id\n+    var item = params.item\n+    var key = params.key\n+\n+    agol.find(id, function (err, data) {\n+      if (err) {\n+        return callback(err, null)\n+      }\n+\n+      // Get the item\n+      if (!parseInt(options.layer, 0)) {\n+        options.layer = 0\n+      }\n+\n+      agol.getItemData(data.host, id, item, key, options, function (error, itemJson) {\n+        if (itemJson && itemJson.koop_status === 'processing' && typeof req.params.silent === 'undefined') {\n+          // callback is never called?\n+          return controller._returnProcessing(req, res, itemJson, callback)\n         }\n-      });  \n-    };\n+        callback(error, itemJson)\n+      })\n+    })\n+  }\n+\n+  /**\n+   * Creates a unique based on request params and the querystring\n+   * @returns {string} key\n+   */\n+  controller._createCacheKey = function (params, query) {\n+    // sort the req.query before we hash so we are consistent\n+    var sorted_query = {}\n+    _(query).keys().sort().each(function (key) {\n+      if (key !== 'url_only' && key !== 'format') {\n+        sorted_query[key] = query[key]\n+      }\n+    })\n+    // build the file key as an MD5 hash that's a join on the paams and look for the file\n+    var toHash = params.item + '_' + (params.layer || 0) + JSON.stringify(sorted_query)\n+\n+    return crypto.createHash('md5').update(toHash).digest('hex')\n+  }\n+\n+  /**\n+   * Finds the item's data\n+   * this method is crux of the controller. It handles the logic for returning data in several forms:\n+   *   - status processing return 202\n+   *   - status is processing but a file exists\n+   *   - a file exists for the data\n+   *   - a new file needs to be created\n+   *   - no format given, just returns json\n+   * @param {object} req - the incoming request object\n+   * @param {object} res - the outgoing response object\n+   */\n+  controller.findItemData = function (req, res) {\n+    var tableKey = controller._createTableKey('agol', req.params)\n+    var dir = req.params.item + '_' + (req.params.layer || 0)\n+    var path\n \n-    // CHECK the time since our last cache entry \n-    // if > 24 hours since; clear cache and wipe files \n-    // else move on\n-    var table_key = ['agol', req.params.item, (req.params.layer || 0)].join(':');\n-    agol.getInfo(table_key, function(err, info){\n+    // returns data in the data\n+    agol.getInfo(tableKey, function (err, info) {\n+      if (err) {\n+        agol.log('error', err)\n+      }\n \n-      // parse the spatial ref if we have one, \n+      // parse the spatial ref if we have one,\n       // if its whitelisted remove it from the query object\n       if (req.query.outSR) {\n-        var sr = agol.parseSpatialReference(req.query.outSR);\n-        if (sr && sr.wkid && [3785, 3857, 4326, 102100].indexOf(sr.wkid) !== -1){\n-          delete req.query.outSR;\n+        var sr = agol.parseSpatialReference(req.query.outSR)\n+        if (sr && sr.wkid && [3785, 3857, 4326, 102100].indexOf(sr.wkid) !== -1) {\n+          delete req.query.outSR\n         }\n       }\n \n-      // sort the req.query before we hash so we are consistent \n-      var sorted_query = {};\n-      _(req.query).keys().sort().each(function (key) {\n-        if (key != 'url_only' && key != 'format'){\n-          sorted_query[key] = req.query[key];\n-        }\n-      });\n+      var key = controller._createCacheKey(req.params, req.query)\n+      req.params.key = key\n \n       // determine if this request is for a filtered dataset\n-      req.query.isFiltered = (req.query.where || req.query.geometry) ? true : false;\n-\n-      // build the file key as an MD5 hash that's a join on the paams and look for the file \n-      var toHash = req.params.item + '_' + ( req.params.layer || 0 ) + JSON.stringify( sorted_query );\n-      var key = crypto.createHash('md5').update(toHash).digest('hex');\n-\n-      var _returnProcessing = function( ){\n-        if (typeof req.params.silent === 'undefined') {\n-          agol.getCount(table_key, {}, function(err, count){\n-            var code = 202;\n-\n-            // we need some logic around handling long standing processing times\n-            var processingTime = ( Date.now() - info.retrieved_at)/1000 || 0;\n-\n-            var response = {\n-              status: 'processing',\n-              processing_time: processingTime, \n-              count: count\n-            };\n-            if ( info.generating ){\n-              response.generating = info.generating;\n-              // we received an error from the server\n-              if (info.generating.error){\n-                code = 502;\n-              }\n-            }\n-            \n-            agol.log('debug',JSON.stringify({status: code, item: req.params.item, layer: ( req.params.layer || 0 )})); \n-            res.status( code ).json( response );\n-          });\n+      req.query.isFiltered = (req.query.where || req.query.geometry)\n+\n+      // if the status is processing we either return with a file or a 202\n+      if (info && info.status === 'processing') {\n+        if (req.params.format) {\n+          return controller._returnProcessingFile(req, res, info)\n         }\n-      };\n+        return controller._returnProcessing(req, res, info)\n+      }\n \n-      if (info && info.status == 'processing'){\n-        if ( req.params.format ) {\n+      // check format for exporting data\n+      if (req.params.format) {\n+        // file params for building an export file\n+        var fileParams = {\n+          req: req,\n+          res: res,\n+          dir: dir,\n+          key: key,\n+          format: req.params.format,\n+          id: req.params.item,\n+          type: 'agol'\n+        }\n \n-          // force an override on the format param if given a format in the query\n-          if ( req.query.format ){\n-            req.params.format = req.query.format;\n-            delete req.query.format;\n-          }\n+        // force an override on the format param if given a format in the query\n+        if (req.query.format) {\n+          req.params.format = req.query.format\n+          delete req.query.format\n+        }\n \n-          // this logic should be wrapped into a Function since its copied from below\n-          req.params.format = req.params.format.replace('geojson', 'json');\n-          var dir = req.params.item + '_' + ( req.params.layer || 0 );\n-          var path = ['files', dir, key].join('/');\n+        // redirect to thumbnail for png access\n+        if (req.params.format === 'png') {\n+          return controller.thumbnail(req, res)\n+        }\n \n-          // get the name of the data; else use the key (md5 hash)\n-          var name = ( info && info.info ) ? info.info.name || info.info.title || info.name : key;\n-          name = (name.length > 150) ? name.substr(0, 150): name;\n-          var fileName = name + '.' + req.params.format;\n-          fileName = fileName.replace(/\\/|,|&|\\|/g, '').replace(/ /g, '_').replace(/\\(|\\)/g, '');\n+        // create the file path\n+        path = controller._createFilePath(key, req.params)\n+        // the file name for the export\n+        fileParams.fileName = controller._createName(info, key, req.params.format)\n \n-          // if we have a layer then append it to the query params \n-          if ( req.params.layer ) {\n-            req.query.layer = req.params.layer;\n-          }\n+        // does the data export already exist?\n+        agol.files.exists(path, fileParams.fileName, function (exists, path) {\n \n-          agol.files.exists( path, fileName, function( exists, path ) {\n-            if ( exists ){ \n-              controller.returnFile(req, res, dir, key, path, name);\n-            } else {\n-              _returnProcessing();\n+          // get the item data before we check for\n+          agol.find(req.params.id, function (err, data) {\n+            if (err) {\n+              return res.status(500).send(err)\n             }\n-          });\n-        } else {\n-          _returnProcessing();\n-        }\n-      } else { \n+            // save the item layer\n+            req.query.layer = (!parseInt(req.params.layer, 0)) ? 0 : req.params.layer\n \n-        // check format for exporting data\n-        if ( req.params.format ){\n+            agol.getItem(data.host, req.params.item, req.query, function (err, itemJson) {\n+              if (err) {\n+                return res.status(500).send(err)\n+              }\n \n-          // force an override on the format param if given a format in the query\n-          if ( req.query.format ){\n-            req.params.format = req.query.format;\n-            delete req.query.format;\n-          }\n+              if (exists) {\n+                return agol.isExpired(info, req.query.layer, function (err, isExpired) {\n+                  fileParams.err = err\n+                  if (!isExpired) {\n+                    return controller._returnFile(req, res, path, fileParams.fileName)\n+                  }\n \n-          // redirect to thumbnail for png access\n-          if (req.params.format == 'png'){\n-            controller.thumbnail(req, res);\n-          } else {\n+                  // if it's expired, then remove the data and request a new file\n+                  agol.dropItem(data.host, req.params.item, req.query, function () {\n+                    req.query.format = req.params.format\n+                    controller._getItemData(req, res, function (err, itemJson) {\n+                      fileParams.err = err\n+                      fileParams.itemJson = itemJson\n+                      fileParams.data = (itemJson && itemJson.data && itemJson.data[0]) ? itemJson.data[0] : null\n+                      // var used to request new files if needed.\n+                      controller._requestNewFile(fileParams)\n+                    })\n+                  })\n+                })\n+              }\n \n-            // change geojson to json\n-            req.params.format = req.params.format.replace('geojson', 'json');\n-            // use the item as the file dir so we can organize exports by id\n-            var dir = req.params.item + '_' + ( req.params.layer || 0 );\n-            var path = [ 'files', dir, key ].join( '/' );\n-            // the file name for the export \n-            var name = ( info && info.info ) ? info.name || info.info.name || info.info.title : key;\n-            name = (name.length > 150) ? name.substr(0, 150): name;\n-            var fileName = name + '.' + req.params.format;\n-            fileName = fileName.replace(/\\/|,|&|\\|/g, '').replace(/ /g, '_').replace(/\\(|\\)/g, '');\n-\n-            // if we have a layer then append it to the query params \n-            if ( req.params.layer ) {\n-              req.query.layer = req.params.layer;\n+              req.query.format = req.params.format\n+              controller._getItemData(req, res, function (err, itemJson) {\n+                fileParams.err = err\n+                fileParams.itemJson = itemJson\n+                fileParams.data = (itemJson && itemJson.data && itemJson.data[0]) ? itemJson.data[0] : null\n+                controller._requestNewFile(fileParams)\n+              })\n+            })\n+          })\n+        })\n+      } else {\n+        // if we have a layer then append it to the query params\n+        if (req.params.layer) {\n+          req.query.layer = req.params.layer\n+        }\n+        // get the esri json data for the service\n+        controller._getItemData(req, res, function (err, itemJson) {\n+          // when silent is sent as a param undefined\n+          if (typeof req.params.silent === 'undefined') {\n+            if (err) {\n+              return res.status(err.code || 404).send(err.error || err)\n             }\n \n-            // does the data export already exist? \n-            agol.files.exists( path, fileName, function( exists, path ){\n-              // get the item data before we check for \n-              agol.find( req.params.id, function( err, data ){\n-                if (err) {\n-                  res.status(500).send( err );\n-                  return;\n-                } else {\n-                  // Get the item\n-                  req.query.layer = ( !parseInt(req.params.layer)) ? 0 : req.params.layer;\n-                   \n-                  // if getMetadata is true then the getItem method will attach the metadata to the json \n-                  req.query.getMetadata = true;\n-                  agol.getItem(data.host, req.params.item, req.query, function( err, itemJson ){\n-                    //agol.getItemData( data.host, req.params.id, req.params.item, key, req.query, function(error, itemJson){\n-                      if ( exists ){\n-                        // check if the cache is expired\n-                        var is_expired = info ? ( new Date().getTime() >= info.expires_at ) : false;\n-                      \n-                        if ( info.info.url ){\n-\n-                          // clean up the url; remove layer at the end just in case \n-                          var url = info.info.url.replace('?f=json','');\n-                          var url_parts = url.split('/');\n-                          var len = url_parts.length - 1;\n-                          if ( parseInt(url_parts[ len ]) >= 0 ){\n-                            var lyrId = url_parts[ len ];\n-                            url = url.substring(0, url.length - ((('' + lyrId ).split('').length || 2)+1));\n-                          }\n-\n-                          agol.getFeatureServiceLayerInfo( url, ( req.params.layer || 0 ), function(err, serviceInfo){\n-                            // check for info on last edit date (for hosted services dont expired unless changed) \n-                            // set is_expired to false if it hasnt changed or if its null\n-                            if ( info && info.retrieved_at && serviceInfo && serviceInfo.editingInfo){\n-                              if (!serviceInfo.editingInfo.lastEditDate && (info.retrieved_at > itemJson.modified)) {\n-                                is_expired = false;\n-                              } else if ( info.retrieved_at < serviceInfo.editingInfo.lastEditDate ){\n-                                is_expired = true;\n-                              } else {\n-                                // if the retrieved at date is greater than the lastEditDate then the data are still good\n-                                is_expired = false;\n-                              }\n-                            }\n-\n-                            // return it.\n-                            // if expired -> remove the data and request\n-                            if ( is_expired ){\n-                              agol.dropItem( data.host, req.params.item, req.query, function( err, success ){\n-                                req.query.format = req.params.format;\n-                                _get(req.params.id, req.params.item, key, req.query, function( err, itemJson ){\n-                                  controller.requestNewFile( req, res, dir, key, err, itemJson );\n-                                });\n-                              });\n-                            } else {\n-                              // else serve it\n-                              controller.returnFile(req, res, dir, key, path, name);\n-                            }\n-                          });\n-\n-                        } else {\n-\n-                          // if expired -> remove the data and request\n-                          if ( is_expired ){\n-                            var d = [dir, key ].join( '/' );\n-                            agol.dropItem( '', req.params.item, req.query, function( err, success ){\n-                              req.query.format = req.params.format;\n-                              _get(req.params.id, req.params.item, key, req.query, function( err, itemJson ){\n-                                controller.requestNewFile( req, res, dir, key, err, itemJson );\n-                              });\n-                            });\n-                          } else {\n-                            // else serve it\n-                            controller.returnFile(req, res, dir, key, path, name);\n-                          }\n-                        }\n-                      } else {\n-                        // check the koop status table to see if we have a job running \n-                          // if we do then return \n-                          // else proceed \n-                        req.query.format = req.params.format;\n-                        _get(req.params.id, req.params.item, key, req.query, function( err, itemJson ){\n-                          controller.requestNewFile( req, res, dir, key, err, itemJson );\n-                        });\n-                      }\n-                    //});\n-                  });\n-                } \n-              });\n-            });\n-          }\n-        } else {\n-          // if we have a layer then append it to the query params \n-          if ( req.params.layer ) {\n-            req.query.layer = req.params.layer;\n+            // TODO remove hard coded maxRecCount\n+            if (itemJson && itemJson.data && itemJson.data[0].features.length > 1000) {\n+              itemJson.data[0].features = itemJson.data[0].features.splice(0, 1000)\n+            }\n+            return res.send(itemJson)\n           }\n-          // get the esri json data for the service\n-          _get(req.params.id, req.params.item, key, req.query, function( err, itemJson ){\n-              // when silent is sent as a param undefined\n-              if (typeof req.params.silent === 'undefined') {\n-                if (err) {\n-                  if ( err.code && err.error ){\n-                    res.status(err.code).send( err.error );\n-                  } else {\n-                    res.status(404).send( err );\n-                  }\n-                } else {\n-                  if ( itemJson && itemJson.data && itemJson.data[0].features.length > 1000){\n-                    itemJson.data[0].features = itemJson.data[0].features.splice(0,1000);\n-                  }\n-                  res.send( itemJson );\n-                }\n-              }\n-          });\n-        }\n+        })\n       }\n-    });\n-  };\n+    })\n+  }\n \n-  controller.requestNewFile = function( req, res, dir, key, err, itemJson ){\n-    if (err){\n-      if ( err.code && err.error ){\n-        res.status(err.code).send( err.error );\n-      } else {\n-        res.status(400).send( err );\n-      }\n-    } else if ( itemJson && itemJson.data && itemJson.data[0] && (!itemJson.data[0].features || !itemJson.data[0].features.length)) {\n-      agol.log('error', req.url +' No features in data');\n-      res.status(404).send( 'No features exist for the requested FeatureService layer');\n-    } else {\n+  /**\n+   * Create key used to query a table in the cache\n+   *\n+   * @params {string} type - the type of the providers (\"agol\")\n+   * @params {object} params - an object with an item id and layer number\n+   *\n+   * @returns {string} key\n+   * @private\n+   */\n+  controller._createTableKey = function (type, params) {\n+    return [type, params.item, (params.layer || 0)].join(':')\n+  }\n \n-      var name = ( itemJson && itemJson.data && itemJson.data[0] && (itemJson.data[0].name || (itemJson.data[0].info && itemJson.data[0].info.name) ) ) ? itemJson.data[0].name || itemJson.data[0].info.name : itemJson.name || itemJson.title;\n-      // cleanze the name\n-      name = name.replace(/\\/|,|&\\|/g, '').replace(/ /g, '_').replace(/\\(|\\)|\\$/g, '');\n-      name = (name.length > 150) ? name.substr(0, 150): name;\n-\n-      var fileParams = {\n-        dir: dir,\n-        key: key, \n-        id: req.params.item,\n-        format: req.params.format,\n-        type: 'agol',\n-        data: (itemJson && itemJson.data && itemJson.data[0]) ? itemJson.data[0] : null\n+  /**\n+   * Respond to a request for file downloads when a dataset is still \"processing\"\n+   * if the file exists send it, else return processing 202\n+   *\n+   * @params {object} type - the type of the providers (\"agol\")\n+   * @params {object} params - an object with an item id and layer number\n+   * @params {object} info - item metadata from the cache\n+   * @private\n+   */\n+  controller._returnProcessingFile = function (req, res, info) {\n+    // force an override on the format param if given a format in the query\n+    delete req.query.format\n+    // create the file path\n+    var path = controller._createFilePath(req.params.key, req.params)\n+    // get the name of the data; else use the key (md5 hash)\n+    var fileName = controller._createName(info, req.params.key, req.params.format)\n+\n+    agol.files.exists(path, fileName, function (exists, path) {\n+      if (exists) {\n+        return controller._returnFile(req, res, path, fileName)\n       }\n+      controller._returnProcessing(req, res, info)\n+    })\n+    return\n+  }\n \n-      if ((itemJson.koop_status && itemJson.koop_status == 'too big') || agol.forceExportWorker ){\n-        // export as a series of small queries/files\n-        var table = 'agol:' + req.params.item + ':' + ( req.params.layer || 0 );\n+  /**\n+   * Respond to a requests with a \"processing\" response\n+   *\n+   * @params {object} type - the type of the providers (\"agol\")\n+   * @params {object} params - an object with an item id and layer number\n+   * @params {object} info - item metadata from the cache\n+   * @private\n+   */\n+  controller._returnProcessing = function (req, res, info) {\n+    var table = controller._createTableKey('agol', req.params)\n \n-        req.query.name = name;\n-        // set the geometry type so the exporter can do its thing for csv points (add x,y)\n-        if (itemJson.data && itemJson.data[0] && itemJson.data[0].info && itemJson.data[0].info.geometryType){\n-          req.query.geomType = itemJson.data[0].info.geometryType;\n+    if (typeof req.params.silent === 'undefined') {\n+      agol.getCount(table, {}, function (err, count) {\n+        if (err) {\n+          return res.status(500).send(err)\n         }\n-        // force export of large data\n-        req.query.large = true\n-\n-        agol.exportFile( fileParams, req.query, function(err, result){\n-          if (result && result.status && result.status == 'processing'){\n-            agol.getCount(table, {}, function(err, count){\n-              var code = 202;\n-              var response = {\n-                status: 'processing',\n-                processing_time: ( Date.now() - result.retrieved_at)/1000 || 0, \n-                count: count\n-              };\n-              if ( result.generating ){\n-                response.generating = result.generating;\n-                // we received an error from the server\n-                if (result.generating.error){\n-                  code = 502;\n-                }\n-              }\n-              res.status(code).json( response );\n-            });\n-          } else if ( req.query.url_only ){\n-            var origUrl = req.originalUrl.split('?');\n-            origUrl[0] = origUrl[0].replace(/json/,req.params.format);\n-            var newUrl = req.protocol +'://'+req.get('host') + origUrl[0] + '?' + origUrl[1].replace(/url_only=true&|url_only=true|/,'').replace('format='+req.params.format,'').replace('&format='+req.params.format,'');\n-            res.json({url: newUrl});\n-           } else {\n-            if (err) {\n-              res.status(err.code || 400).send( err );\n-            } else {\n-              if (req.params.format == 'json' || req.params.format == 'geojson'){\n-                res.contentType('text');\n-              } \n-              res.sendfile(result);\n-            }\n-          }\n-        });\n-      } else if (itemJson && itemJson.data && itemJson.data[0]) {\n+        var code = 202\n \n-        var opts = {\n-          isFiltered: req.query.isFiltered, \n-          name: name, \n-          outSR: req.query.outSR\n-        };\n+        // we need some logic around handling long standing processing times\n+        var processingTime = (Date.now() - info.retrieved_at) / 1000 || 0\n \n-        if (itemJson.metadata) {\n-          opts.metadata = itemJson.metadata;\n+        var response = {\n+          status: 'processing',\n+          processing_time: processingTime,\n+          count: count\n+        }\n+        if (info.generating) {\n+          response.generating = info.generating\n+          if (info.generating.error) {\n+            code = 502\n+          }\n         }\n \n-        agol.exportFile(fileParams, opts, function(err, result){\n-          if ( req.query.url_only ){\n-            var origUrl = req.originalUrl.split('?');\n-            origUrl[0] = origUrl[0].replace(/json/,req.params.format);\n-            var newUrl = req.protocol +'://'+req.get('host') + origUrl[0] + '?' + origUrl[1].replace(/url_only=true&|url_only=true|/,'').replace('format='+req.params.format,'').replace('&format='+req.params.format,'');\n+        agol.log('debug', JSON.stringify({status: code, item: req.params.item, layer: (req.params.layer || 0)}))\n+        res.status(code).json(response)\n+      })\n+    }\n+  }\n \n-            res.json({url: newUrl});\n-          } else {\n-            if (err) {\n-              res.status(err.code || 400).send( err );\n-            } else {\n-              res.setHeader('Content-disposition', 'attachment; filename='+(encodeURIComponent(name)+'.'+req.params.format));\n-              if (req.params.format == 'json' || req.params.format == 'geojson'){\n-                res.contentType('application/json');\n-              } else if (req.params.format == 'kml'){\n-                res.contentType('application/vnd.google-earth.kml+xml');\n-              } else if ( req.params.format == 'csv'){\n-                res.contentType('text/csv');\n-              } else if ( req.params.format == 'zip'){\n-                res.contentType('application/octet-stream');\n-              }\n+  /**\n+   * Creates a clean and normalized name to be used in things like files on disk\n+   *\n+   * @params {string} type - the type of the providers (\"agol\")\n+   * @params {object} params - an object with an item id and layer number\n+   *\n+   * @returns {string} key\n+   * @private\n+   */\n+  controller._createName = function (info, key, format) {\n+    format = format.replace('geojson', 'json')\n \n-              if ( result.substr(0,4) == 'http' ){\n-                // Proxy to s3 urls allows us to not show the URL \n-                https.get(result, function(proxyRes) {\n-                  proxyRes.pipe(res);\n-                });\n-              } else {\n-                res.sendfile(result);\n-              }\n-            }\n-          }\n-        });\n-      } else {\n-        res.status(400).send( 'Could not create export, missing data' );\n+    var name = (info && info.info) ? info.name || info.info.name || info.info.title : key\n+    name = (name.length > 150) ? name.substr(0, 150) : name\n+\n+    var fileName = name + '.' + format\n+    fileName = fileName.replace(/\\/|,|&|\\|/g, '').replace(/ /g, '_').replace(/\\(|\\)/g, '')\n+\n+    return fileName\n+  }\n+\n+  /**\n+   * Creates a clean file path for files exports\n+   *\n+   * @params {string} type - the type of the providers (\"agol\")\n+   * @params {object} params - an object with an item id and layer number\n+   *\n+   * @returns {string} key\n+   * @private\n+   */\n+  controller._createFilePath = function (key, params) {\n+    var dir = params.item + '_' + (params.layer || 0)\n+    return ['files', dir, key].join('/')\n+  }\n+\n+  /**\n+   * Create key used to query a table in the cache\n+   *\n+   * @params {string} type - the type of the providers (\"agol\")\n+   * @params {object} params - an object with an item id and layer number\n+   *\n+   * @returns {string} key\n+   * @private\n+   */\n+  controller._requestNewFile = function (params) {\n+    var res = params.res\n+    var itemJson = params.itemJson\n+\n+    if (params.err) {\n+      return res.status(params.err.code || 400).send(params.err.error || params.err)\n+    }\n+\n+    // flatten the data from an array to sep objects/arrays\n+    var itemData\n+    var itemFeatures\n+    if (itemJson && itemJson.data && itemJson.data[0]) {\n+      itemData = itemJson.data[0]\n+    }\n+\n+    if (itemData && itemData.features) {\n+      itemFeatures = itemData.features\n+    }\n+\n+    if (!itemFeatures || !itemFeatures.length) {\n+      return res.status(404).send('No features exist for the requested FeatureService layer')\n+    }\n+\n+    // this logic sure does suck...\n+    var name = (itemJson && itemJson.data && itemJson.data[0] && (itemJson.data[0].name || (itemJson.data[0].info && itemJson.data[0].info.name))) ? itemJson.data[0].name || itemJson.data[0].info.name : itemJson.name || itemJson.title\n+    // cleanze the name\n+    name = name.replace(/\\/|,|&\\|/g, '').replace(/ /g, '_').replace(/\\(|\\)|\\$/g, '')\n+    name = (name.length > 150) ? name.substr(0, 150) : name\n+    params.name = name\n+\n+    if ((itemJson.koop_status && itemJson.koop_status === 'too big') || agol.forceExportWorker) {\n+      return controller._exportLarge(params)\n+    }\n+\n+    if (itemJson && itemJson.data && itemJson.data[0]) {\n+      return controller._exportToFormat(params)\n+    }\n+\n+    return res.status(400).send('Could not create export, missing data')\n+  }\n+\n+  /**\n+   * Exports a large dataset\n+   * calls the exportLarge method on the agol model\n+   * @param {objects} params - file export parameters\n+   */\n+  controller._exportLarge = function (params) {\n+    var req = params.req\n+    var res = params.res\n+    var itemJson = params.itemJson\n+    req.query.name = params.name\n+\n+    // set the geometry type so the exporter can do its thing for csv points (add x,y)\n+    if (itemJson.data && itemJson.data && itemJson.data.info && itemJson.data.info.geometryType) {\n+      req.query.geomType = itemJson.data[0].info.geometryType\n+    }\n+\n+    // force export of large data\n+    req.query.large = true\n+\n+    agol.exportFile(params, req.query, function (err, result) {\n+      if (err) {\n+        return res.status(err.code || 400).send(err)\n       }\n+\n+      if (result && result.status && result.status === 'processing') {\n+        return controller._returnProcessing(req, res, itemJson)\n+      }\n+\n+      if (req.query.url_only) {\n+        // reuse this code...\n+        var origUrl = req.originalUrl.split('?')\n+        origUrl[0] = origUrl[0].replace(/json/, req.params.format)\n+\n+        var newUrl = req.protocol + '://' + req.get('host') + origUrl[0] + '?' + origUrl[1]\n+          .replace(/url_only=true&|url_only=true|/, '')\n+          .replace('format=' + req.params.format, '')\n+          .replace('&format=' + req.params.format, '')\n+\n+        return res.json({url: newUrl})\n+      }\n+      res.contentType('text')","path":"controller/index.js","position":1001,"original_position":1001,"commit_id":"02042f8355cd4ed978e28fe869f578e33dbcbb40","original_commit_id":"02042f8355cd4ed978e28fe869f578e33dbcbb40","user":{"login":"dmfenton","id":7832202,"avatar_url":"https://avatars.githubusercontent.com/u/7832202?v=3","gravatar_id":"","url":"https://api.github.com/users/dmfenton","html_url":"https://github.com/dmfenton","followers_url":"https://api.github.com/users/dmfenton/followers","following_url":"https://api.github.com/users/dmfenton/following{/other_user}","gists_url":"https://api.github.com/users/dmfenton/gists{/gist_id}","starred_url":"https://api.github.com/users/dmfenton/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dmfenton/subscriptions","organizations_url":"https://api.github.com/users/dmfenton/orgs","repos_url":"https://api.github.com/users/dmfenton/repos","events_url":"https://api.github.com/users/dmfenton/events{/privacy}","received_events_url":"https://api.github.com/users/dmfenton/received_events","type":"User","site_admin":false},"body":"again, what's with the text content type?","created_at":"2015-07-31T13:17:49Z","updated_at":"2015-07-31T13:17:49Z","html_url":"https://github.com/koopjs/koop-agol/pull/46#discussion_r35971879","pull_request_url":"https://api.github.com/repos/koopjs/koop-agol/pulls/46","_links":{"self":{"href":"https://api.github.com/repos/koopjs/koop-agol/pulls/comments/35971879"},"html":{"href":"https://github.com/koopjs/koop-agol/pull/46#discussion_r35971879"},"pull_request":{"href":"https://api.github.com/repos/koopjs/koop-agol/pulls/46"}}},"pull_request":{"url":"https://api.github.com/repos/koopjs/koop-agol/pulls/46","id":41066413,"html_url":"https://github.com/koopjs/koop-agol/pull/46","diff_url":"https://github.com/koopjs/koop-agol/pull/46.diff","patch_url":"https://github.com/koopjs/koop-agol/pull/46.patch","issue_url":"https://api.github.com/repos/koopjs/koop-agol/issues/46","number":46,"state":"open","locked":false,"title":"[DNM] Refactoring the model and controller and worker","user":{"login":"chelm","id":351164,"avatar_url":"https://avatars.githubusercontent.com/u/351164?v=3","gravatar_id":"","url":"https://api.github.com/users/chelm","html_url":"https://github.com/chelm","followers_url":"https://api.github.com/users/chelm/followers","following_url":"https://api.github.com/users/chelm/following{/other_user}","gists_url":"https://api.github.com/users/chelm/gists{/gist_id}","starred_url":"https://api.github.com/users/chelm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/chelm/subscriptions","organizations_url":"https://api.github.com/users/chelm/orgs","repos_url":"https://api.github.com/users/chelm/repos","events_url":"https://api.github.com/users/chelm/events{/privacy}","received_events_url":"https://api.github.com/users/chelm/received_events","type":"User","site_admin":false},"body":"Okay this is pretty big. It's not done yet but tests pass. Problems is that test are not awesome. I'm to spend another day working on getting meaningful tests before we want to merge.","created_at":"2015-07-28T22:04:36Z","updated_at":"2015-07-31T13:17:50Z","closed_at":null,"merged_at":null,"merge_commit_sha":"bd2930496fa4a72e94f6a0df651b6480c3c78d7d","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/koopjs/koop-agol/pulls/46/commits","review_comments_url":"https://api.github.com/repos/koopjs/koop-agol/pulls/46/comments","review_comment_url":"https://api.github.com/repos/koopjs/koop-agol/pulls/comments{/number}","comments_url":"https://api.github.com/repos/koopjs/koop-agol/issues/46/comments","statuses_url":"https://api.github.com/repos/koopjs/koop-agol/statuses/02042f8355cd4ed978e28fe869f578e33dbcbb40","head":{"label":"koopjs:model-refactor","ref":"model-refactor","sha":"02042f8355cd4ed978e28fe869f578e33dbcbb40","user":{"login":"koopjs","id":11509517,"avatar_url":"https://avatars.githubusercontent.com/u/11509517?v=3","gravatar_id":"","url":"https://api.github.com/users/koopjs","html_url":"https://github.com/koopjs","followers_url":"https://api.github.com/users/koopjs/followers","following_url":"https://api.github.com/users/koopjs/following{/other_user}","gists_url":"https://api.github.com/users/koopjs/gists{/gist_id}","starred_url":"https://api.github.com/users/koopjs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/koopjs/subscriptions","organizations_url":"https://api.github.com/users/koopjs/orgs","repos_url":"https://api.github.com/users/koopjs/repos","events_url":"https://api.github.com/users/koopjs/events{/privacy}","received_events_url":"https://api.github.com/users/koopjs/received_events","type":"Organization","site_admin":false},"repo":{"id":19042497,"name":"koop-agol","full_name":"koopjs/koop-agol","owner":{"login":"koopjs","id":11509517,"avatar_url":"https://avatars.githubusercontent.com/u/11509517?v=3","gravatar_id":"","url":"https://api.github.com/users/koopjs","html_url":"https://github.com/koopjs","followers_url":"https://api.github.com/users/koopjs/followers","following_url":"https://api.github.com/users/koopjs/following{/other_user}","gists_url":"https://api.github.com/users/koopjs/gists{/gist_id}","starred_url":"https://api.github.com/users/koopjs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/koopjs/subscriptions","organizations_url":"https://api.github.com/users/koopjs/orgs","repos_url":"https://api.github.com/users/koopjs/repos","events_url":"https://api.github.com/users/koopjs/events{/privacy}","received_events_url":"https://api.github.com/users/koopjs/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/koopjs/koop-agol","description":"ArcGIS Online provider for Koop (public services only).","fork":false,"url":"https://api.github.com/repos/koopjs/koop-agol","forks_url":"https://api.github.com/repos/koopjs/koop-agol/forks","keys_url":"https://api.github.com/repos/koopjs/koop-agol/keys{/key_id}","collaborators_url":"https://api.github.com/repos/koopjs/koop-agol/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/koopjs/koop-agol/teams","hooks_url":"https://api.github.com/repos/koopjs/koop-agol/hooks","issue_events_url":"https://api.github.com/repos/koopjs/koop-agol/issues/events{/number}","events_url":"https://api.github.com/repos/koopjs/koop-agol/events","assignees_url":"https://api.github.com/repos/koopjs/koop-agol/assignees{/user}","branches_url":"https://api.github.com/repos/koopjs/koop-agol/branches{/branch}","tags_url":"https://api.github.com/repos/koopjs/koop-agol/tags","blobs_url":"https://api.github.com/repos/koopjs/koop-agol/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/koopjs/koop-agol/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/koopjs/koop-agol/git/refs{/sha}","trees_url":"https://api.github.com/repos/koopjs/koop-agol/git/trees{/sha}","statuses_url":"https://api.github.com/repos/koopjs/koop-agol/statuses/{sha}","languages_url":"https://api.github.com/repos/koopjs/koop-agol/languages","stargazers_url":"https://api.github.com/repos/koopjs/koop-agol/stargazers","contributors_url":"https://api.github.com/repos/koopjs/koop-agol/contributors","subscribers_url":"https://api.github.com/repos/koopjs/koop-agol/subscribers","subscription_url":"https://api.github.com/repos/koopjs/koop-agol/subscription","commits_url":"https://api.github.com/repos/koopjs/koop-agol/commits{/sha}","git_commits_url":"https://api.github.com/repos/koopjs/koop-agol/git/commits{/sha}","comments_url":"https://api.github.com/repos/koopjs/koop-agol/comments{/number}","issue_comment_url":"https://api.github.com/repos/koopjs/koop-agol/issues/comments{/number}","contents_url":"https://api.github.com/repos/koopjs/koop-agol/contents/{+path}","compare_url":"https://api.github.com/repos/koopjs/koop-agol/compare/{base}...{head}","merges_url":"https://api.github.com/repos/koopjs/koop-agol/merges","archive_url":"https://api.github.com/repos/koopjs/koop-agol/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/koopjs/koop-agol/downloads","issues_url":"https://api.github.com/repos/koopjs/koop-agol/issues{/number}","pulls_url":"https://api.github.com/repos/koopjs/koop-agol/pulls{/number}","milestones_url":"https://api.github.com/repos/koopjs/koop-agol/milestones{/number}","notifications_url":"https://api.github.com/repos/koopjs/koop-agol/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/koopjs/koop-agol/labels{/name}","releases_url":"https://api.github.com/repos/koopjs/koop-agol/releases{/id}","created_at":"2014-04-22T19:09:53Z","updated_at":"2015-07-28T22:27:20Z","pushed_at":"2015-07-30T20:41:11Z","git_url":"git://github.com/koopjs/koop-agol.git","ssh_url":"git@github.com:koopjs/koop-agol.git","clone_url":"https://github.com/koopjs/koop-agol.git","svn_url":"https://github.com/koopjs/koop-agol","homepage":"","size":2935,"stargazers_count":3,"watchers_count":3,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":4,"mirror_url":null,"open_issues_count":7,"forks":4,"open_issues":7,"watchers":3,"default_branch":"master"}},"base":{"label":"koopjs:master","ref":"master","sha":"c73e344e940a525452e3cc1012814bd7a8361c4e","user":{"login":"koopjs","id":11509517,"avatar_url":"https://avatars.githubusercontent.com/u/11509517?v=3","gravatar_id":"","url":"https://api.github.com/users/koopjs","html_url":"https://github.com/koopjs","followers_url":"https://api.github.com/users/koopjs/followers","following_url":"https://api.github.com/users/koopjs/following{/other_user}","gists_url":"https://api.github.com/users/koopjs/gists{/gist_id}","starred_url":"https://api.github.com/users/koopjs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/koopjs/subscriptions","organizations_url":"https://api.github.com/users/koopjs/orgs","repos_url":"https://api.github.com/users/koopjs/repos","events_url":"https://api.github.com/users/koopjs/events{/privacy}","received_events_url":"https://api.github.com/users/koopjs/received_events","type":"Organization","site_admin":false},"repo":{"id":19042497,"name":"koop-agol","full_name":"koopjs/koop-agol","owner":{"login":"koopjs","id":11509517,"avatar_url":"https://avatars.githubusercontent.com/u/11509517?v=3","gravatar_id":"","url":"https://api.github.com/users/koopjs","html_url":"https://github.com/koopjs","followers_url":"https://api.github.com/users/koopjs/followers","following_url":"https://api.github.com/users/koopjs/following{/other_user}","gists_url":"https://api.github.com/users/koopjs/gists{/gist_id}","starred_url":"https://api.github.com/users/koopjs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/koopjs/subscriptions","organizations_url":"https://api.github.com/users/koopjs/orgs","repos_url":"https://api.github.com/users/koopjs/repos","events_url":"https://api.github.com/users/koopjs/events{/privacy}","received_events_url":"https://api.github.com/users/koopjs/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/koopjs/koop-agol","description":"ArcGIS Online provider for Koop (public services only).","fork":false,"url":"https://api.github.com/repos/koopjs/koop-agol","forks_url":"https://api.github.com/repos/koopjs/koop-agol/forks","keys_url":"https://api.github.com/repos/koopjs/koop-agol/keys{/key_id}","collaborators_url":"https://api.github.com/repos/koopjs/koop-agol/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/koopjs/koop-agol/teams","hooks_url":"https://api.github.com/repos/koopjs/koop-agol/hooks","issue_events_url":"https://api.github.com/repos/koopjs/koop-agol/issues/events{/number}","events_url":"https://api.github.com/repos/koopjs/koop-agol/events","assignees_url":"https://api.github.com/repos/koopjs/koop-agol/assignees{/user}","branches_url":"https://api.github.com/repos/koopjs/koop-agol/branches{/branch}","tags_url":"https://api.github.com/repos/koopjs/koop-agol/tags","blobs_url":"https://api.github.com/repos/koopjs/koop-agol/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/koopjs/koop-agol/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/koopjs/koop-agol/git/refs{/sha}","trees_url":"https://api.github.com/repos/koopjs/koop-agol/git/trees{/sha}","statuses_url":"https://api.github.com/repos/koopjs/koop-agol/statuses/{sha}","languages_url":"https://api.github.com/repos/koopjs/koop-agol/languages","stargazers_url":"https://api.github.com/repos/koopjs/koop-agol/stargazers","contributors_url":"https://api.github.com/repos/koopjs/koop-agol/contributors","subscribers_url":"https://api.github.com/repos/koopjs/koop-agol/subscribers","subscription_url":"https://api.github.com/repos/koopjs/koop-agol/subscription","commits_url":"https://api.github.com/repos/koopjs/koop-agol/commits{/sha}","git_commits_url":"https://api.github.com/repos/koopjs/koop-agol/git/commits{/sha}","comments_url":"https://api.github.com/repos/koopjs/koop-agol/comments{/number}","issue_comment_url":"https://api.github.com/repos/koopjs/koop-agol/issues/comments{/number}","contents_url":"https://api.github.com/repos/koopjs/koop-agol/contents/{+path}","compare_url":"https://api.github.com/repos/koopjs/koop-agol/compare/{base}...{head}","merges_url":"https://api.github.com/repos/koopjs/koop-agol/merges","archive_url":"https://api.github.com/repos/koopjs/koop-agol/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/koopjs/koop-agol/downloads","issues_url":"https://api.github.com/repos/koopjs/koop-agol/issues{/number}","pulls_url":"https://api.github.com/repos/koopjs/koop-agol/pulls{/number}","milestones_url":"https://api.github.com/repos/koopjs/koop-agol/milestones{/number}","notifications_url":"https://api.github.com/repos/koopjs/koop-agol/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/koopjs/koop-agol/labels{/name}","releases_url":"https://api.github.com/repos/koopjs/koop-agol/releases{/id}","created_at":"2014-04-22T19:09:53Z","updated_at":"2015-07-28T22:27:20Z","pushed_at":"2015-07-30T20:41:11Z","git_url":"git://github.com/koopjs/koop-agol.git","ssh_url":"git@github.com:koopjs/koop-agol.git","clone_url":"https://github.com/koopjs/koop-agol.git","svn_url":"https://github.com/koopjs/koop-agol","homepage":"","size":2935,"stargazers_count":3,"watchers_count":3,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":4,"mirror_url":null,"open_issues_count":7,"forks":4,"open_issues":7,"watchers":3,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/koopjs/koop-agol/pulls/46"},"html":{"href":"https://github.com/koopjs/koop-agol/pull/46"},"issue":{"href":"https://api.github.com/repos/koopjs/koop-agol/issues/46"},"comments":{"href":"https://api.github.com/repos/koopjs/koop-agol/issues/46/comments"},"review_comments":{"href":"https://api.github.com/repos/koopjs/koop-agol/pulls/46/comments"},"review_comment":{"href":"https://api.github.com/repos/koopjs/koop-agol/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/koopjs/koop-agol/pulls/46/commits"},"statuses":{"href":"https://api.github.com/repos/koopjs/koop-agol/statuses/02042f8355cd4ed978e28fe869f578e33dbcbb40"}}}}