[
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n"
  },
  {
    "path": ".gitignore",
    "content": "lib-cov\n*.seed\n*.log\n*.csv\n*.dat\n*.out\n*.pid\n*.gz\n\npids\nlogs\nresults\n\nnpm-debug.log\nnode_modules\n\n*.sublime-project\n*.sublime-workspace\n\nbuild\n"
  },
  {
    "path": ".jshintrc",
    "content": "{\n  \"bitwise\": true,\n  \"curly\": true,\n  \"eqeqeq\": true,\n  \"esnext\": true,\n  \"expr\": true,\n  \"forin\": false,\n  \"immed\": true,\n  \"indent\": 2,\n  \"latedef\": true,\n  \"loopfunc\": true,\n  \"newcap\": false,\n  \"noarg\": true,\n  \"quotmark\": false,\n  \"regexp\": true,\n  \"scripturl\": true,\n  \"smarttabs\": true,\n  \"strict\": true,\n  \"sub\": true,\n  \"trailing\": true,\n  \"undef\": true,\n  \"unused\": true,\n\n  // Predefined globals that JSHint will ignore.\n  \"node\": true,\n  \"browser\": true\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2014, Lincoln Loop\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright\n      notice, this list of conditions and the following disclaimer in the\n      documentation and/or other materials provided with the distribution.\n    * Neither the name of the <organization> nor the\n      names of its contributors may be used to endorse or promote products\n      derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
  },
  {
    "path": "README.md",
    "content": "![Amygdala logo](https://raw.githubusercontent.com/lincolnloop/amygdala/master/static/logo.png)\n\nAmygdala is a RESTful HTTP library for JavaScript powered web applications. Simply configure it once with your API schema, and easily do GET, POST, PUT and DELETE requests with minimal effort and a consistent API.\n\nExamples:\n\n```javascript\n// GET\nstore.get('users').done(function() { ... });\n\n// POST\nstore.add('teams', {'name': 'Lincoln Loop', 'active': true}).done(function() { ... });\n```\n\n[![browser support](https://ci.testling.com/lincolnloop/amygdala.png)\n](https://ci.testling.com/lincolnloop/amygdala)\n\n## How it works\n\n### 1. INSTALL\n\n\n#### NPM/Browserify\n\n`npm install amygdala`.\n\n\n#### Bower\n\n`bower install amygdala`\n\n\n#### Browser\n\nDownload the latest [amygdala.js](https://github.com/lincolnloop/amygdala/blob/master/amygdala.js) file. Minified/build version coming soon.\n\n##### Dependencies:\n\n* [lodash](https://lodash.com): ^3.10.1\n* [q](https://github.com/kriskowal/q): ^1.0.1\n* [Wolfy87/EventEmitter](https://github.com/Wolfy87/EventEmitter): ^4.2.6\n\n\n### 2. SETUP\n\nTo create a new store, define the few possible settings listed below and your API schema.\n\n```javascript\nvar store = new Amygdala({\n  'config': {\n    'apiUrl': 'http://localhost:8000',\n    'idAttribute': 'url',\n    'headers': {\n      'X-CSRFToken': getCookie('csrftoken')\n    },\n    'localStorage': true\n  },\n  'schema': {\n    'users': {\n      'url': '/api/v2/user/'\n    },\n    'teams': {\n      'url': '/api/v2/team/',\n      'orderBy': 'name',\n      'oneToMany': {\n        'members': 'members'\n      },\n      parse: function(data) {\n        return data.results ? data.results : data;\n      },\n    },\n    'members': {\n      'foreignKey': {\n        'user': 'users'\n      }\n    }\n  }\n});\n```\n\n#### Configuration options:\n\n  * apiUrl - Full path to your base API url (required).\n  * idAttribute - global primary key attribute (required). \n  * headers - Any headers that you need to pass on each API request.\n  * localStorage - enable/disable the persistent localStorage cache.\n\n#### Schema options:\n  \n  * url - relative path for each \"table\" (required)\n  * orderBy - order by which you want to retrieve local cached data. eg (name, -name (for reverse))\n  * parse - Accepts a parse method for cases when your API also returns extra meta data.\n  * idAttribute - overrides key attribute (if different in this schema)\n\n\n#### Schema relations:\n\nWhen you want to include related data under a single request, for example, to minimize HTTP requests, having schema relations allows you to still have a clean separation when interacting with the data locally.\n\nConsider the following schema, that defines discussions that have messages, and messages that have votes:\n\n```javascript\nvar store = new Amygdala({\n    'config': {\n      'apiUrl': 'http://localhost:8000',\n      'idAttribute': 'url'\n    },\n    'schema': {\n      'discussions': {\n        'url': '/api/v2/discussion/',\n        'oneToMany': {\n          'children': 'messages'\n        }\n      },\n      'messages': {\n        'url': '/api/v2/message/',\n        'oneToMany': {\n          'votes': 'votes'\n        },\n        'foreignKey': {\n          'discussion': 'discussions'\n        }\n      },\n      'votes': {\n        'url': '/api/v2/vote/'\n      }\n    }\n  }\n);\n```\n\nIn this scenario, doing a query on a discussion will retrieve all messages and votes for that discussion:\n\n```javascript\nstore.get('discussions', {'url': '/api/v2/discussion/85273/'}).then(function(){ ... });\n```\n\nSince we defined relations on our schema, the message and vote data won't be stored on the discussion \"table\", but on it's own \"table\" instead.\n\n##### OneToMany:\n\n```javascript\n'oneToMany': {\n  'children': 'messages'\n}\n```\n\n`OneToMany` relations are the most common, and should be used when you have related data in form of an array. In this case, `children` is the attribute name on the response, and `messages` is the destination \"table\" for the array data.\n\n\n##### foreignKey:\n\n```javascript\n'foreignKey': {\n  'discussion': 'discussions'\n}\n```\n\n`foreignKey` relations are basically for one to one relations. In this case Amygdala will look for an object as value of `discussion` and move it over to the `discussions` \"table\" if one is found.\n\n\n### 3. USAGE\n\n#### Querying the remote API server:\n\nThe methods below, allow you to make remote calls to your API server.\n\n```javascript\n// GET\nstore.get('users').done(function() { ... });\n\n// POST\nstore.add('teams', {'name': 'Lincoln Loop', 'active': true}).done(function() { ... });\n\n// PUT\nstore.update('users', {'url': '/api/v2/user/32/', 'username': 'amy82', 'active': true}).done(function() { ... });\n\n// DELETE\nstore.remove('users', {'url': '/api/v2/user/32/'}).done(function() { ... });\n```\n\n\n#### In memory storage API:\n\nOn top of this, Amygdala also stores a copy of your data locally, which you can access through a couple different methods:\n\n\n###### Find and filtering:\n\n```javascript\n// Get the list of active users from memory\nvar users = store.findAll('users', {'active': true});\n\n// Get a single user from memory\nvar user = store.find('users', {'username': 'amy82'});\n\n// Get a single user by id for memory\nvar user = store.find('users', 1103747470);\n```\n\nIf you enable `localStorage`, the data is kept persistently. Because of this, once you instantiate Amygdala, your cached data will be loaded, and you can use it right away without having to wait for the remote calls. (We do not recommend using `localStorage` for production yet)\n\n\n##### Fetching related data:\n\nBy defining your schema and creating relations between data, you are then able to query your data objects for the related objects.\n\nIn the example schema above, discussions have a oneToMany relation with messages, and messages have a foreignKey relation back to discussions. This is how it you can use them.\n\n```javascript\n// Fetching related messages for a discussion (oneToMay)\nvar messages = store.find('discussions', '/api/v2/discussion/85273/').getRelated('messages');\n\n// Getting the discussion object from a message (foreignKey)\nvar discussion = store.find('message', '/api/v2/message/81273/').getRelated('discussion');\n```\n\nNote that Amygdala doesn't fetch data automagically for you here, so it's up you to fetch it before running the query.\n\n## Events\n\nAmygdala uses [Wolfy87/EventEmitter](https://github.com/Wolfy87/EventEmitter) under the hood\nto trigger some very basic events. Right now it only triggers two different events:\n\n* change\n* change:type\n\nTo listen to these events, you can use any of [Event Emitter's](https://github.com/Wolfy87/EventEmitter/blob/master/docs/guide.md#using-eventemitterr) binding methods or the [aliases](https://github.com/Wolfy87/EventEmitter/blob/master/docs/guide.md#method-aliases), the most common one being `on`:\n\n```javascript\n// Listen to any change in the store\nstore.on('change', function() { ... });\n\n// Listen to any change of a specific type\nstore.on('change:users', function() { ... });\n```\n"
  },
  {
    "path": "amygdala.js",
    "content": "'use strict';\n\n// CommonJS check so we can require dependencies\nif (typeof module === 'object' && module.exports) {\n  var each = require('lodash/collection/each');\n  var partial = require('lodash/function/partial');\n  var debounce = require('lodash/function/debounce');\n  var clone = require('lodash/lang/clone');\n  var isString = require('lodash/lang/isString');\n  var isObject = require('lodash/lang/isObject');\n  var isFunction = require('lodash/lang/isFunction');\n  var isArray = require('lodash/lang/isArray');\n  var isEmpty = require('lodash/lang/isEmpty');\n  var defaults = require('lodash/object/defaults');\n  var map = require('lodash/collection/map');\n  var filter = require('lodash/collection/filter');\n  var findWhere = require('lodash/collection/findWhere');\n  var sortBy = require('lodash/collection/sortBy');\n  var Q = require('q');\n  var EventEmitter = require('wolfy87-eventemitter');\n}\n\nvar Amygdala = function(options) {\n  // Initialize a new Amygdala instance with the given schema and options.\n  //\n  // params:\n  // - options (Object)\n  //   - config (apiUrl, headers)\n  //   - schema\n  this._config = options.config;\n  this._schema = options.schema;\n  this._headers = this._config.headers;\n\n  // if not apiUrl is defined, use current location origin\n  if (!this._config.apiUrl) {\n    this._config.apiUrl = window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '');\n  }\n\n  // memory data storage\n  this._store = {};\n  this._changeEvents = {};\n\n  if (this._config.localStorage) {\n    each(this._schema, function(value, key) {\n      // check each schema entry for localStorage data\n      // TODO: filter out apiUrl and idAttribute \n      var storageCache = window.localStorage.getItem('amy-' + key);\n      if (storageCache) {\n        this._set(key, JSON.parse(storageCache), {'silent': true} );\n      }\n    }.bind(this));\n\n    // store every change on local storage\n    // when localStorage is set to true\n    this.on('change', function(type) {\n      this.setCache(type, this.findAll(type));\n    }.bind(this));\n  }\n};\n\nAmygdala.prototype = clone(EventEmitter.prototype);\n\n// ------------------------------\n// Helper methods\n// ------------------------------\nAmygdala.prototype.serialize = function serialize(obj) {\n  // Translates an object to a querystring\n\n  if (!isObject(obj)) {\n    return obj;\n  }\n  var pairs = [];\n  each(obj, function(value, key) {\n    pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));\n  });\n  return pairs.join('&');\n}\n\nAmygdala.prototype.ajax = function ajax(method, url, options) {\n   // Sends an Ajax request, converting the data into a querystring if the\n   // method is GET.\n   //\n   // params:\n   // -method (string): GET, POST, PUT, DELETE\n   // -url (string): The url to send the request to.\n   // -options (Object)\n   //\n   // options\n   // - data (Object): Will be converted to a querystring for GET requests.\n   // - contentType (string): A value for the Content-Type request header.\n   // - headers (Object): Additional headers to add to the request.\n  var query;\n  options = options || {};\n\n  if (!isEmpty(options.data) && method === 'GET') {\n    query = this.serialize(options.data);\n    url = url + '?' + query;\n  }\n\n  var request = new XMLHttpRequest();\n  var deferred = Q.defer();\n\n  request.open(method, url, true);\n\n  request.onload = function() {\n    // status 200 OK, 201 CREATED, 20* ALL OK\n    if (request.status.toString().substr(0, 2) === '20') {\n      deferred.resolve(request);\n    } else {\n      deferred.reject(request);\n    }\n  };\n\n  request.onerror = function() {\n    deferred.reject(new Error('Unabe to send request to ' + JSON.stringify(url)));\n  };\n\n  if (!isEmpty(options.contentType)) {\n    request.setRequestHeader('Content-Type', options.contentType);\n  }\n\n  if (!isEmpty(options.headers)) {\n    each(options.headers, function(value, key) {\n      if (isFunction(value)) {\n        request.setRequestHeader(key,value());\n      }\n      else {\n        request.setRequestHeader(key, value);\n      }\n    });\n  }\n\n  request.send(method === 'GET' ? null : options.data);\n\n  return deferred.promise;\n}\n\n// ------------------------------\n// Internal utils methods\n// ------------------------------\nAmygdala.prototype._getURI = function(type, params) {\n  var url;\n  // get absolute uri for api endpoint\n  if (!this._schema[type] || !this._schema[type].url) {\n    throw new Error('Invalid type. Acceptable types are: ' + Object.keys(this._schema));\n  }\n  url = this._config.apiUrl + this._schema[type].url;\n\n  // if the `idAttribute` specified by the schema or config\n  // exists as a key in `params` append it's value to the url,\n  // and remove it from `params` so it's not sent in the query string.\n  if (params && this._getIdAttribute(type) in params) {\n    url += params[this._getIdAttribute(type)];\n    delete params[this._getIdAttribute(type)];\n  }\n\n  return url;\n},\n\nAmygdala.prototype._getIdAttribute = function(type) {\n  // schema may override idAttribute\n  return this._schema[type].idAttribute || this._config.idAttribute;\n},\n\nAmygdala.prototype._emitChange = function(type) {\n\n  // TODO: Add tests for debounced events\n  if (!this._changeEvents[type]) {\n    this._changeEvents[type] = debounce(partial(function(type) {\n      // emit changes events\n      this.emit('change', type);\n      // change:<type>\n      this.emit('change:' + type);\n      // TODO: compare the previous object and trigger change events\n    }.bind(this), type), 150);\n  }\n  \n  this._changeEvents[type]();\n}\n\n// ------------------------------\n// Internal data sync methods\n// ------------------------------\nAmygdala.prototype._set = function(type, response, options) {\n  // Adds or Updates an item of `type` in this._store.\n  //\n  // type: schema key/store (teams, users)\n  // ajaxResponse: response to store in local cache\n\n  // initialize store for this type (if needed)\n  // and store it under `store` for easy access.\n  var store = this._store[type] ? this._store[type] : this._store[type] = {};\n  var schema = this._schema[type];\n  var wrappedResponse = false;\n\n  if (isString(response)) {\n    // If the response is a string, try JSON.parse.\n    try {\n      response = JSON.parse(response);\n    } catch(e) {\n      throw('Invalid JSON from the API response.');\n    }\n  }\n\n  if (!isArray(response)) {\n    // The response isn't an array. We need to figure out how to handle it.\n    if (schema.parse) {\n      // Prefer the schema's parse method if one exists.\n      response = schema.parse(response);\n      // if it's still not an array, wrap it around one\n      if (!isArray(response)) {\n        response = [response];\n      }\n    } else {\n      // Otherwise, just wrap it in an array and hope for the best.\n      response = [response];\n      wrappedResponse = true;\n    }\n  }\n\n  each(response, function(obj) {\n    // store the object under this._store['type']['id']\n    store[obj[this._getIdAttribute(type)]] = obj;\n\n    // handle oneToMany relations\n    each(this._schema[type].oneToMany, function(relatedType, relatedAttr) {\n      var related = obj[relatedAttr];\n      // check if obj has a `relatedAttr` that is defined as a relation\n      if (related) {\n        // check if attr value is an array,\n        // if it's not empty, and if the content is an object and not a string\n        if (Object.prototype.toString.call(related) === '[object Array]' &&\n          related.length > 0 &&\n          Object.prototype.toString.call(related[0]) === '[object Object]') {\n          // if related is a list of objects,\n          // populate the relation `table` with this data\n          this._set(relatedType, related);\n          // and replace the list of objects within `obj`\n          // by a list of `id's\n          obj[relatedAttr] = map(related, function(item) {\n            return item[this._getIdAttribute(type)];\n          }.bind(this));\n        }\n      }\n    }.bind(this));\n\n    // handle foreignKey relations\n    each(this._schema[type].foreignKey, function(relatedType, relatedAttr) {\n      var related = obj[relatedAttr];\n      // check if obj has a `relatedAttr` that is defined as a relation\n      if (related) {\n        // check if `obj[relatedAttr]` value is an object (FK should not be arrays),\n        // if it's not empty, and if the content is an object and not a string\n        if (Object.prototype.toString.call(related) === '[object Object]') {\n          // if related is an object,\n          // populate the relation `table` with this data\n          this._set(relatedType, [related]);\n          // and replace the list of objects within `item`\n          // by a list of `id's\n          obj[relatedAttr] = related[this._getIdAttribute(type)];\n        }\n      }\n    }.bind(this));\n\n    // obj.related()\n    // set up a related method to fetch other related objects\n    // as defined in the schema for the store.\n    obj.getRelated = partial(function(schema, obj, attributeName) {\n      if (schema.oneToMany && attributeName in schema.oneToMany) {\n        //\n        // if oneToMany relation\n        //\n        // loop through each id in the obj\n        // and return the full related object list as the response\n        return obj[attributeName].map(function(value) {\n          // find in related `table` by id\n          return this.find(schema.oneToMany[attributeName], value);\n        }.bind(this)).filter(function(value) {\n          // filter out undefined/null values\n          return !!value;\n        });\n      } else if (schema.foreignKey && attributeName in schema.foreignKey) {\n        //\n        // else, if foreignKey relation\n        //\n        //\n        // find in related `table` by id\n        return this.find(schema.foreignKey[attributeName], obj[attributeName]);\n      }\n      return null;\n    }.bind(this), schema, obj);\n\n    // emit change events\n    if (!options || options.silent !== true) {\n      this._emitChange(type);\n    }\n\n  }.bind(this));\n\n  // return our data as the original api call's response\n  return wrappedResponse && response.length === 1 ? response[0] : response;\n};\n\nAmygdala.prototype._setAjax = function(type, request, options) {\n  return this._set(type, request.response, options);\n}\n\nAmygdala.prototype._remove = function(type, object) {\n  // Removes an item of `type` from this._store.\n  //\n  // type: schema key/store (teams, users)\n  // response: response to store in local cache\n\n  this._emitChange(type);\n\n  // delete object of type by id\n  delete this._store[type][object[this._getIdAttribute(type)]]\n};\n\nAmygdala.prototype._validateURI = function(url) {\n  // convert paths to full URLs\n  // TODO: DRY UP\n  if (url.indexOf('/') === 0) {\n    return this._config.apiUrl + url;\n  }\n\n  return url;\n}\n\n// ------------------------------\n// Public data sync methods\n// ------------------------------\nAmygdala.prototype._get = function(url, params) {\n  // AJAX post request wrapper\n  // TODO: make this method public in the future\n\n  // Request settings\n  var settings = {\n    'data': params,\n    'headers': this._headers\n  };\n\n  return this.ajax('GET', this._validateURI(url), settings);\n}\n\nAmygdala.prototype.get = function(type, params, options) {\n  // GET request for `type` with optional `params`\n  //\n  // type: schema key/store (teams, users)\n  // params: extra queryString params (?team=xpto&user=xyz)\n  // options: extra options\n  // - url: url override\n\n  // Default to the URI for 'type'\n  options = options || {};\n  defaults(options, {'url': this._getURI(type, params)});\n\n  return this._get(options.url, params)\n    .then(partial(this._setAjax, type).bind(this));\n};\n\nAmygdala.prototype._post = function(url, data) {\n  // AJAX post request wrapper\n  // TODO: make this method public in the future\n\n  // Request settings\n  var settings = {\n    'data': data ? JSON.stringify(data) : null,\n    'contentType': 'application/json',\n    'headers': this._headers\n  };\n\n  return this.ajax('POST', this._validateURI(url), settings);\n}\n\nAmygdala.prototype.add = function(type, object, options) {\n  // POST/PUT request for `object` in `type`\n  //\n  // type: schema key/store (teams, users)\n  // object: object to update local and remote\n  // options: extra options\n  // -  url: url override\n\n  // Default to the URI for 'type'\n  options = options || {};\n  defaults(options, {'url': this._getURI(type)});\n\n  // Dynamic URL is now accepted in post\n  object.url ? options.url = object.url : null;\n  \n  return this._post(options.url, object)\n    .then(partial(this._setAjax, type).bind(this));\n};\n\nAmygdala.prototype._put = function(url, data) {\n  // AJAX put request wrapper\n  // TODO: make this method public in the future\n\n  // Request settings\n  var settings = {\n    'data': JSON.stringify(data),\n    'contentType': 'application/json',\n    'headers': this._headers\n  };\n\n  return this.ajax('PUT', this._validateURI(url), settings);\n}\n\nAmygdala.prototype.update = function(type, object) {\n  // POST/PUT request for `object` in `type`\n  //\n  // type: schema key/store (teams, users)\n  // object: object to update local and remote\n  var url = object.url;\n\n  if (!url && this._getIdAttribute(type) in object) {\n    url = this._getURI(type, object);\n  }\n\n  if (!url) {\n    throw new Error('Missing required object.url or ' + this._getIdAttribute(type) + ' attribute.');\n  }\n\n  return this._put(url, object)\n    .then(partial(this._setAjax, type).bind(this));\n};\n\nAmygdala.prototype._delete = function(url, data) {\n  // AJAX delete request wrapper\n  // TODO: make this method public in the future\n  var settings = {\n    'data': JSON.stringify(data),\n    'contentType': 'application/json',\n    'headers': this._headers\n  };\n\n  return this.ajax('DELETE', this._validateURI(url), settings);\n}\n\nAmygdala.prototype.remove = function(type, object) {\n  // DELETE request for `object` in `type`\n  //\n  // type: schema key/store (teams, users)\n  // object: object to update local and remote\n  var url = object.url;\n\n  if (!url && this._getIdAttribute(type) in object) {\n    url = this._getURI(type, object);\n  }\n\n  if (!url) {\n    throw new Error('Missing required object.url or ' + this._getIdAttribute(type) + ' attribute.');\n  }\n\n  return this._delete(url, object)\n    .then(partial(this._remove, type, object).bind(this));\n};\n\n// ------------------------------\n// Public cache methods\n// ------------------------------\nAmygdala.prototype.setCache = function(type, objects) {\n  if (!type) {\n    throw new Error('Missing schema type parameter.');\n  }\n  if (!this._schema[type]) {\n    throw new Error('Invalid type. Acceptable types are: ' + Object.keys(this._schema));\n  }\n  return window.localStorage.setItem('amy-' + type, JSON.stringify(objects));\n};\n\nAmygdala.prototype.getCache = function(type) {\n  if (!type) {\n    throw new Error('Missing schema type parameter.');\n  }\n  if (!this._schema[type] || !this._schema[type].url) {\n    throw new Error('Invalid type. Acceptable types are: ' + Object.keys(this._schema));\n  }\n  return JSON.parse(window.localStorage.getItem('amy-' + type));\n};\n\n// ------------------------------\n// Public query methods\n// ------------------------------\nAmygdala.prototype.findAll = function(type, query) {\n  // find a list of items within the store. (THAT ARE NOT STORED IN BACKBONE COLLECTIONS)\n  var store = this._store[type];\n  var orderBy;\n  var reverseMatch;\n  var results;\n  if (!store || !Object.keys(store).length) {\n    return [];\n  }\n  if (query === undefined) {\n    // query is empty, no object is returned\n    results = map(store, function(item) { return item; });\n  } else if (Object.prototype.toString.call(query) === '[object Object]') {\n    // if query is an object, assume it specifies filters.\n    results = filter(store, function(item) { return findWhere([item], query); });\n  } else {\n    throw new Error('Invalid query for findAll.');\n  }\n  orderBy = this._schema[type].orderBy;\n  if (orderBy) {\n    // match the orderBy attribute for the presence\n    // of a reverse flag\n    reverseMatch = orderBy.match(/^-([\\w-]{0,})$/);\n    if (reverseMatch !== null) {\n      // if we have two matches, we have a reverse flag\n      orderBy = orderBy.replace('-', '');\n    }\n    results = sortBy(results, function(item) {\n      return item[orderBy].toString().toLowerCase();\n    }.bind(this));\n\n    if (reverseMatch !== null) {\n      // reverse the results\n      results = results.reverse();\n    }\n  }\n  return results;\n};\n\nAmygdala.prototype.find = function(type, query) {\n  // find a specific within the store. (THAT ARE NOT STORED IN BACKBONE COLLECTIONS)\n  var store = this._store[type];\n  if (!store || !Object.keys(store).length) {\n    return undefined;\n  }\n  if (query === undefined) {\n    // query is empty, no object is returned\n    return  undefined;\n  } else if (Object.prototype.toString.call(query) === '[object Object]') {\n    // if query is an object, return the first match for the query\n    return findWhere(store, query);\n  } else {\n    // if query is a String or Number, assume it stores the key/url value\n    // Object.prototype.toString.call(query) === '[object String]'\n    // Object.prototype.toString.call(query) === '[object Number]'\n    return store[query];\n  }\n};\n\n// expose via CommonJS, AMD or as a global object\nif (typeof module === 'object' && module.exports) {\n  module.exports = Amygdala;\n} else if (typeof define === 'function' && define.amd) {\n  define(function() {\n    return Amygdala;\n  });\n} else {\n  window.Amygdala = Amygdala;\n}\n"
  },
  {
    "path": "bower.json",
    "content": "{\n  \"name\": \"Amygdala\",\n  \"main\": \"amygdala.js\",\n  \"version\": \"0.4.5\",\n  \"homepage\": \"https://github.com/lincolnloop/amygdala\",\n  \"authors\": [\n    \"Marco Louro <marco@lincolnloop.com> (http://lincolnloop.com)\"\n  ],\n  \"description\": \"RESTful HTTP library for JavaScript powered web applications\",\n  \"keywords\": [\n    \"REST\",\n    \"client\",\n    \"http\",\n    \"API\",\n    \"localStorage\",\n    \"store\",\n    \"browser\",\n    \"library\",\n    \"cache\",\n    \"ajax\",\n    \"offline\"\n  ],\n  \"license\": \"BSD\",\n  \"dependencies\": {\n    \"underscore\"  : \">=1.6.0\",\n    \"q\": \">=1.0.1\",\n    \"eventEmitter\": \">=4.2.6\"\n  }\n}\n"
  },
  {
    "path": "gulpfile.js",
    "content": "'use strict';\n\nvar _ = require('lodash');\nvar browserify = require('gulp-browserify');\nvar gulp = require('gulp');\nvar pkg = require('./package.json');\nvar rename = require('gulp-rename');\nvar uglify = require('gulp-uglify');\nvar wrap = require('gulp-wrap');\n\ngulp.task('default', function() {\n  // Set the environment to production\n  process.env.NODE_ENV = 'production';\n  gulp.start('distribute');\n});\n\ngulp.task('build', function() {\n  var production = process.env.NODE_ENV === 'production';\n\n  var stream = gulp.src('./amygdala.js')\n\n    // Browserify, and add source maps if this isn't a production build\n    .pipe(browserify({debug: !production}))\n\n    .on('prebundle', function(bundler) {\n      if (production) {\n        // Externalize dependencies so they aren't included in the build\n        bundler.external('underscore');\n        bundler.external('q');\n\n        // Export Amygdala as 'amygdala'\n        bundler.require('./amygdala.js', {expose: 'amygdala'});\n      }\n    })\n\n    // Rename the destination file\n    .pipe(rename(pkg.name + '.js'));\n\n  if (production) {\n    // Wrap in a UMD template\n    stream.pipe(wrap({src: 'templates/umd.jst'}, {\n      pkg: pkg,\n      namespace: 'Amygdala',\n      deps: {\n        'underscore': '_',\n        'q': 'Q'\n      },\n      expose: 'amygdala'\n    }, {'imports': {'_': _}}));\n  }\n\n  // Dist directory if production, otherwise the ignored build dir\n  stream.pipe(gulp.dest(production ? 'dist/' : 'build/'));\n\n  return stream;\n});\n\ngulp.task('distribute', ['build'], function() {\n  gulp.src('dist/' + pkg.name + '.js')\n    .pipe(uglify())\n    .pipe(rename(pkg.name + '.min.js'))\n    .pipe(gulp.dest('dist/'));\n});\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"amygdala\",\n  \"version\": \"0.5.0\",\n  \"description\": \"RESTful HTTP library for JavaScript powered web applications\",\n  \"keywords\": [\n    \"REST\",\n    \"client\",\n    \"http\",\n    \"API\",\n    \"localStorage\",\n    \"store\",\n    \"browser\",\n    \"library\",\n    \"cache\",\n    \"ajax\",\n    \"offline\"\n  ],\n  \"homepage\": \"https://github.com/lincolnloop/amygdala\",\n  \"bugs\": {\n    \"url\": \"https://github.com/lincolnloop/amygdala/issues\"\n  },\n  \"author\": \"Marco Louro <marco@lincolnloop.com> (http://lincolnloop.com)\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git://github.com/lincolnloop/amygdala.git\"\n  },\n  \"main\": \"amygdala.js\",\n  \"files\": [\n    \"amygdala.js\"\n  ],\n  \"scripts\": {\n    \"test\": \"mocha --reporter spec --ui bdd\"\n  },\n  \"testling\": {\n    \"harness\": \"mocha-bdd\",\n    \"files\": \"test/*.test.js\",\n    \"browsers\": [\n      \"ie/9..latest\",\n      \"chrome/26..latest\",\n      \"firefox/22..latest\",\n      \"safari/latest\",\n      \"opera/12.0..latest\",\n      \"iphone/latest\",\n      \"ipad/latest\",\n      \"android-browser/latest\"\n    ]\n  },\n  \"licenses\": [\n    {\n      \"type\": \"BSD\",\n      \"url\": \"http://github.com/lincolnloop/amygdala/blob/master/LICENSE\"\n    }\n  ],\n  \"devDependencies\": {\n    \"chai\": \"^1.9.0\",\n    \"mocha\": \"^1.17.1\",\n    \"sinon\": \"^1.9.0\",\n    \"sinon-chai\": \"^2.5.0\",\n    \"xmlhttprequest\": \"^1.6.0\",\n    \"gulp-rename\": \"^1.2.0\",\n    \"gulp-wrap\": \"^0.3.0\",\n    \"gulp\": \"^3.5.6\",\n    \"gulp-browserify\": \"^0.5.0\",\n    \"gulp-uglify\": \"^0.2.1\"\n  },\n  \"dependencies\": {\n    \"lodash\": \"^3.10.0\",\n    \"q\": \"^1.0.1\",\n    \"wolfy87-eventemitter\": \"^4.2.6\"\n  }\n}\n"
  },
  {
    "path": "templates/umd.jst",
    "content": "/*\n * Amygdala v<%= pkg.version %>\n * (c) <%= new Date().getFullYear() %> <%= pkg.author %>\n * <%= pkg.homepage %>\n * Licensed under the <%= pkg.licenses[0].type %> license.\n * <%= pkg.licenses[0].url %>\n */\n<% var commonDeps = _.map(_.keys(deps), function(dep) {return \"require('\" + dep + \"')\";}); %>\n<% var globalDeps = _.map(_.values(deps), function(dep) {return \"root.\" + dep;}); %>\n<% var requireMap = _.map(_.pairs(deps), function(dep) {return dep[0] + ': ' + dep[1];}); %>\n(function (root, factory) {\n  if (typeof define === 'function' && define.amd) {\n    // AMD. Register as an anonymous module.\n    define(<%= _.keys(deps).join(', ') %>factory);\n  } else if (typeof exports === 'object') {\n    // Node. Does not work with strict CommonJS, but\n    // only CommonJS-like environments that support module.exports,\n    // like Node.\n    module.exports = factory(<%= commonDeps.join(', ') %>);\n  } else {\n    // Browser globals (root is window)\n    root.<%= namespace %> = factory(<%= globalDeps.join(', ') %>);\n  }\n}(this, function (<%= _.values(deps).join(', ') %>) {\n\n  // A shim for 'require' so that it will work universally for externals\n  var require = function(name) {\n    return {<%= requireMap.join(', ') %>}[name];\n  };\n\n/*\n * -------- Begin module --------\n */\n<%= contents %>\n/*\n * -------- End module --------\n */\n\n  return require('<%= expose %>');\n\n}));\n"
  },
  {
    "path": "test/amygdala.test.js",
    "content": "/* global describe, it, before, after, beforeEach */\n'use strict';\n\nvar _ = require('lodash');\nvar chai = require('chai');\nvar sinon = require('sinon');\nvar sinonChai = require('sinon-chai');\nvar Amygdala = require('../amygdala');\n\n// fixtures\nvar teamFixtures = require('./fixtures/teams');\nvar userFixtures = require('./fixtures/users');\nvar discussionFixtures = require('./fixtures/discussions');\n\n// Setup\nvar expect = chai.expect;\nchai.use(sinonChai);\n// log.setLevel('debug');\n\ndescribe('Amygdala', function() {\n\n  var store, authStore, xhr;\n  var settings = {\n    'config': {\n      'apiUrl': 'http://localhost:8000',\n      'idAttribute': 'url'\n    },\n    'schema': {\n      'teams': {\n        'url': '/api/v2/team/',\n        'oneToMany': {\n          'members': 'members'\n        }\n      },\n      'users': {\n        'url': '/api/v2/user/'\n      },\n      'members': {\n        'foreignKey': {\n          'user': 'users'\n        }\n      },\n      'attachments': {\n        'url': '/api/v2/attachment/',\n        'foreignKey': {\n          'user': 'users',\n          'message': 'messages'\n        }\n      },\n      'discussions': {\n        'url': '/api/v2/discussion/',\n        'orderBy': 'title',\n        'foreignKey': {\n          'message': 'messages',\n          'team': 'teams'\n        },\n        parse: function(data) {\n          return data.results;\n        },\n      },\n      'messages': {\n        'url': '/api/v2/message/',\n        'oneToMany': {\n          'attachments': 'attachments',\n          'votes': 'votes'\n        },\n        'foreignKey': {\n          'user': 'users',\n          'discussion': 'discussions'\n        }\n      },\n      'votes': {\n        'url': '/api/v2/vote/',\n        'foreignKey': {\n          'message': 'messages'\n        }\n      }\n    }\n  };\n\n  before(function() {\n    store = new Amygdala(settings);\n    store._set('users', userFixtures);\n    store._set('teams', teamFixtures);\n    store._set('discussions', discussionFixtures);\n\n    var authSettings = {\n      'config': {\n        'apiUrl': 'http://localhost:8000',\n        'idAttribute': 'url',\n        'headers': {'Authorization': 'alpha'}\n      },\n      'schema': settings.schema\n    };\n\n    authStore = new Amygdala(authSettings);\n    authStore._set('users', userFixtures);\n    authStore._set('teams', teamFixtures);\n    authStore._set('discussions', discussionFixtures);\n\n    global.XMLHttpRequest = function() {\n      return xhr;\n    };\n  });\n\n  beforeEach(function() {\n    // Reset the xhr spy between each test\n    xhr = {\n      'open': sinon.spy(),\n      'send': sinon.spy(),\n      'setRequestHeader': sinon.spy()\n    };\n  });\n\n  after(function() {\n    delete global.XMLHttpRequest;\n  });\n\n  describe('#_set()', function() {\n\n    it('loads simple models correctly', function() {\n      expect(Object.keys(store._store['teams'])).to.have.length(1);\n    });\n\n    it('populates tables based on one-to-many relations', function() {\n      expect(Object.keys(store._store['members'])).to.have.length(3);\n    });\n\n    it('replaces objects by id\\'s in one-to-many relations', function() {\n      expect(\n        store._store.teams['/api/v2/team/9/'].members\n          .indexOf('/api/v2/team/9/member/f31abb30271cdecae75a6227128c8fd9/')\n      ).to.not.equal(-1);\n    });\n\n    it('replaces objects by id\\'s in foreign-key relations', function() {\n      expect(\n        store._store.discussions['/api/v2/discussion/595/'].message\n      ).to.equal('/api/v2/message/3798/');\n    });\n\n    it('sets up a helper method to get related objects', function() {\n      expect(store.find('teams', '/api/v2/team/9/').getRelated).to.exist;\n    });\n\n    it('uses data parsers when they are defined', function() {\n      // discussions have a non-standard data structure due to pagintation,\n      // so the schema provides a `parse` method.\n      // for storage purposes we only want the objects, not the meta data.\n      expect(Object.keys(store._store['discussions'])).to.have.length(4);\n    });\n\n    it('attempts to parse JSON if the format of the response is a string', function() {\n      // Create an empty store for this test\n      var jsonStore = new Amygdala(settings);\n\n      // Set the users with a JSON string\n      jsonStore._set('users', JSON.stringify(userFixtures));\n\n      expect(Object.keys(jsonStore._store['users'])).to.have.length(3);\n      expect(_.pluck(jsonStore._store['users'], 'name')).to.contain('Brandon Konkle');\n    });\n\n    it('Single item result lists are returned correctly', function() {\n      // Create an empty store for this test\n      var jsonStore = new Amygdala(settings);\n\n      // Set the users with a JSON string\n      jsonStore._set('users', [userFixtures[0]]);\n\n      expect(Object.keys(jsonStore._store['users'])).to.have.length(1);\n      expect(_.pluck(jsonStore._store['users'], 'name')).to.contain('Martin');\n    });\n\n    it('Single item result is returned correctly', function() {\n      // Create an empty store for this test\n      var jsonStore = new Amygdala(settings);\n\n      // Set the users with a JSON string\n      jsonStore._set('users', userFixtures[0]);\n      expect(Object.keys(jsonStore._store['users'])).to.have.length(1);\n      \n      jsonStore._set('users', userFixtures[1]);\n      expect(Object.keys(jsonStore._store['users'])).to.have.length(2);\n      \n      expect(_.pluck(jsonStore._store['users'], 'name')).to.contain('Martin');\n    });\n\n    it('will throw an error including the string if the JSON parse fails', function() {\n      // Create an empty store for this test\n      var jsonStore = new Amygdala(settings);\n\n      // Set the users with an invalid string\n      var invalidSet = function() {\n        jsonStore._set('users', 'JSON fiesta!');\n      };\n\n      expect(invalidSet).to.throw('Invalid JSON from the API response.');\n    });\n\n  });\n\n  describe('#<obj>.getRelated(<attributeName>)', function() {\n\n    it('returns a list of objects based on the oneToMany relation', function() {\n      expect(store.find('messages', '/api/v2/message/3798/').getRelated('votes')).to.have.length(1);\n    });\n\n    it('returns an object for foreignKey relations', function() {\n      expect(store.find('messages', '/api/v2/message/3789/').getRelated('discussion').title)\n        .to.equal('unicode');\n    });\n\n  });\n\n  describe('#get()', function() {\n\n    it('triggers an Ajax GET request', function() {\n      store.get('discussions', {'id': 1});\n\n      expect(xhr.open).to.have.been.calledOnce;\n      expect(xhr.open).to.have.been.calledWith('GET', 'http://localhost:8000/api/v2/discussion/?id=1', true);\n      expect(xhr.send).to.have.been.calledOnce;\n      expect(xhr.send).to.have.been.calledWith(null);\n    });\n\n    it('calls #_set() with the given type', function(done) {\n      var originalSet = store._set;\n      store._set = sinon.spy();\n\n      store.get('discussions', {'id': 1})\n        .then(function() {\n          expect(store._set).to.have.been.calledWith('discussions', 'response');\n\n          // Clean up\n          store._set = originalSet;\n          done();\n        }).catch(function(error) {\n          // Catch and report errors\n          done(error);\n        });\n\n      // Set the status, then trigger the resolution of the promise so the\n      // 'then' block above is executed.\n      xhr.status = 200;\n      xhr.response = 'response';\n      xhr.onload();\n    });\n\n    it('doesn\\'t add any headers by default', function() {\n      store.get('discussions', {'id': 1});\n\n      expect(xhr.setRequestHeader).to.not.have.been.called;\n    });\n\n    it('will add headers if Amygdala was initialized with some', function() {\n      authStore.get('discussions', {'id': 1});\n\n      expect(xhr.setRequestHeader).to.have.been.calledOnce\n        .and.have.been.calledWith('Authorization', 'alpha');\n    });\n\n  });\n\n  describe('#add()', function() {\n    var obj = {'name': 'The Alliance'};\n\n    it('triggers an Ajax POST request', function() {\n      store.add('teams', obj);\n\n      expect(xhr.open).to.have.been.calledOnce\n        .and.have.been.calledWith('POST', 'http://localhost:8000/api/v2/team/', true);\n      expect(xhr.send).to.have.been.calledOnce\n        .and.have.been.calledWith(JSON.stringify(obj));\n    });\n\n    it('calls #_set() with the given type', function(done) {\n      var originalSet = store._set;\n      store._set = sinon.spy();\n\n      store.add('teams', obj)\n        .then(function() {\n          expect(store._set).to.have.been.calledWith('teams', 'response');\n\n          // Clean up\n          store._set = originalSet;\n          done();\n        }).catch(function(error) {\n          // Catch and report errors\n          done(error);\n        });\n\n      // Set the status, then trigger the resolution of the promise so the\n      // 'then' block above is executed.\n      xhr.status = 200;\n      xhr.response = 'response';\n      xhr.onload();\n    });\n\n    it('only adds the Content-Type header by deault', function() {\n      store.add('teams', obj);\n\n      expect(xhr.setRequestHeader).to.have.been.calledOnce\n        .and.have.been.calledWith('Content-Type', 'application/json');\n    });\n\n    it('will add headers if Amygdala was initialized with some', function() {\n      authStore.add('teams', obj);\n\n      expect(xhr.setRequestHeader).to.have.been.calledTwice\n        .and.have.been.calledWith('Authorization', 'alpha');\n    });\n    var obj2 = {'url': '/api/v2/team/anotherURL','name': 'The Alliance'};\n\n\t  it('triggers an Ajax POST request with custom url', function() {\n\t\t  store.add('teams', obj2);\n\n\t\t  expect(xhr.open).to.have.been.calledOnce\n\t\t    .and.have.been.calledWith('POST', 'http://localhost:8000/api/v2/team/anotherURL', true);\n\t\t  expect(xhr.send).to.have.been.calledOnce\n\t\t    .and.have.been.calledWith(JSON.stringify(obj2));\n\t  });\n\n  });\n\n  describe('#update()', function() {\n    var obj = {'title': 'Rise of the Horde', 'url': '/draenor/'};\n\n    it('triggers an Ajax PUT request', function() {\n\n      store.update('messages', obj);\n\n      expect(xhr.open).to.have.been.calledOnce;\n      expect(xhr.open).to.have.been.calledWith('PUT', 'http://localhost:8000/draenor/', true);\n      expect(xhr.send).to.have.been.calledOnce;\n      expect(xhr.send).to.have.been.calledWith(JSON.stringify(obj));\n    });\n\n    it('calls #_set() with the given type', function(done) {\n      var originalSet = store._set;\n      store._set = sinon.spy();\n\n      store.update('messages', obj)\n        .then(function() {\n          expect(store._set).to.have.been.calledWith('messages', 'response');\n\n          // Clean up\n          store._set = originalSet;\n          done();\n        }).catch(function(error) {\n          // Catch and report errors\n          done(error);\n        });\n\n      // Set the status, then trigger the resolution of the promise so the\n      // 'then' block above is executed.\n      xhr.status = 200;\n      xhr.response = 'response';\n      xhr.onload();\n    });\n\n    it('only adds the Content-Type header by deault', function() {\n      store.update('messages', obj);\n\n      expect(xhr.setRequestHeader).to.have.been.calledOnce\n        .and.have.been.calledWith('Content-Type', 'application/json');\n    });\n\n    it('will add headers if Amygdala was initialized with some', function() {\n      authStore.update('messages', obj);\n\n      expect(xhr.setRequestHeader).to.have.been.calledTwice\n        .and.have.been.calledWith('Authorization', 'alpha');\n    });\n\n  });\n\n  describe('#remove()', function() {\n    var obj = discussionFixtures.results[0];\n\n    it('triggers an Ajax DELETE request', function() {\n      store.remove('discussions', obj);\n\n      expect(xhr.open).to.have.been.calledOnce;\n      expect(xhr.open).to.have.been.calledWith('DELETE', 'http://localhost:8000/api/v2/discussion/595/', true);\n      expect(xhr.send).to.have.been.calledOnce;\n      expect(xhr.send).to.have.been.calledWith(JSON.stringify(obj));\n    });\n\n    it('calls #_remove() with the given type', function(done) {\n      var originalRemove = store._remove;\n      store._remove = sinon.spy();\n\n      store.remove('discussions', obj)\n        .then(function() {\n          expect(store._remove).to.have.been.calledWith('discussions', obj);\n          // Clean up\n          store._remove = originalRemove;\n          done();\n        }).catch(function(error) {\n          // Catch and report errors\n          done(error);\n        });\n\n      // Set the status, then trigger the resolution of the promise so the\n      // 'then' block above is executed.\n      xhr.status = 200;\n      xhr.response = 'response';\n      xhr.onload();\n    });\n\n    it('removes the object from the cached store', function(done) {\n\n      store.remove('discussions', obj)\n        .then(function() {\n          expect(store.find('discussions', obj.url)).to.equal(undefined);\n          // add the discussion back into the store\n          store._set('discussions', discussionFixtures);\n          done();\n        }).catch(function(error) {\n          // Catch and report errors\n          done(error);\n        });\n\n      // Set the status, then trigger the resolution of the promise so the\n      // 'then' block above is executed.\n      xhr.status = 200;\n      xhr.response = 'response';\n      xhr.onload();\n    });\n\n    it('only adds the Content-Type header by deault', function() {\n      store.remove('discussions', obj);\n\n      expect(xhr.setRequestHeader).to.have.been.calledOnce\n        .and.have.been.calledWith('Content-Type', 'application/json');\n    });\n\n    it('will add headers if Amygdala was initialized with some', function() {\n      authStore.remove('discussions', obj);\n\n      expect(xhr.setRequestHeader).to.have.been.calledTwice\n        .and.have.been.calledWith('Authorization', 'alpha');\n    });\n\n  });\n\n  describe('#findAll()', function() {\n\n    it('can find a list of type', function() {\n      expect(store.findAll('discussions'))\n        .to.have.length(4);\n    });\n\n    it('can find a list of type with filters', function() {\n      expect(store.findAll('discussions', {'intro': 'unicode'}))\n        .to.have.length(1);\n    });\n\n  });\n\n  describe('#find()', function() {\n\n    it('can find an object by id', function() {\n      expect(store.find('teams', '/api/v2/team/9/').name)\n        .to.equal('Test Sandbox');\n    });\n\n    it('can find an object with filters', function() {\n      expect(store.find('discussions', {'intro': 'unicode'}).title)\n        .to.equal('unicode');\n    });\n\n  });\n\n  describe('#orderBy', function() {\n\n    it('can sort', function() {\n      expect(store.findAll('discussions')[0].title)\n        .to.equal('bleep bloop');\n    });\n\n    it('can reverse sort', function() {\n      expect(store.find('discussions', {'intro': 'unicode'}).title)\n        .to.equal('unicode');\n    });\n\n  });\n\n  describe('custom idAttribute', function() {\n    // clone the settings, so changes don't affect the other tests\n    var customSettings = _.clone(settings);\n    // make sure we clone config too, so it does not override\n    // the default config for the other tests.\n    customSettings.config = _.extend(_.clone(customSettings.config), {'idAttribute': 'id'});\n    // instatiate the customStore\n    var customStore = new Amygdala(customSettings);\n\n    it('is sent on GET requests as part of the url and not as a query string', function() {\n      customStore.get('teams', {'id': 31});\n\n      expect(xhr.open).to.have.been.calledOnce\n        .and.have.been.calledWith('GET', 'http://localhost:8000/api/v2/team/31', true);\n    });\n\n    it('is set on PUT requests as part of the url and not as data', function() {\n      customStore.update('teams', {'id': 31});\n\n      expect(xhr.open).to.have.been.calledOnce\n        .and.have.been.calledWith('PUT', 'http://localhost:8000/api/v2/team/31', true);\n    });\n\n    it('is set on DELETE requests as part of the url and not as data', function() {\n      customStore.remove('teams', {'id': 31});\n\n      expect(xhr.open).to.have.been.calledOnce\n        .and.have.been.calledWith('DELETE', 'http://localhost:8000/api/v2/team/31', true);\n    });\n\n  });\n\n  /*\n  describe('^events', function() {\n\n    it('triggers a change:<type> event when an object of <type> is added', function() {\n      var callback = sinon.spy();\n      // register the event\n      store.on('change:teams', callback);\n      // trigger the event on add\n      store._set('teams', {'name': 'The <type> Event Team'});\n\n      expect(callback).to.have.been.calledOnce;\n    });\n\n    it('triggers a change event when an object is changed', function() {\n      var callback = sinon.spy();\n      // register the event\n      store.on('change', callback);\n      // trigger the event on add\n      store._set('teams', {\n        'url': '/api/v2/team/9/',\n        'name': 'The Event Team'\n      });\n\n      expect(callback).to.have.been.calledOnce;\n    });\n\n    it('triggers one change event for multiple changes of the same type', function() {\n      var callback = sinon.spy();\n      // register the event\n      store.on('change', callback);\n      // trigger the event on add\n      store._set('teams', {\n        'url': '/api/v2/team/9/',\n        'name': 'The Event Team'\n      });\n      store._set('teams', {\n        'url': '/api/v2/team/10/',\n        'name': 'Zee Loop'\n      });\n\n      expect(callback).to.have.been.calledOnce;\n    });\n\n    it('triggers two events for changes in different types', function() {\n      var callback = sinon.spy();\n      // register the event\n      store.on('change', callback);\n      // trigger the event on add\n      store._set('teams', {\n        'url': '/api/v2/team/9/',\n        'name': 'The Event Team'\n      });\n      store._set('users', {\n        'url': '/api/v2/user/10/',\n        'name': 'Me Robot'\n      });\n\n      expect(callback).to.have.been.calledTwice;\n    });\n\n    it('triggers a change event when an object is deleted', function() {\n      var callback = sinon.spy();\n      // register the event\n      store.on('change', callback);\n      // trigger the event on add\n      store._remove('teams', {\n        'url': '/api/v2/team/9/',\n        'name': 'The Event Team'\n      });\n\n      expect(callback).to.have.been.calledOnce;\n    });\n\n  });\n  */\n\n});\n"
  },
  {
    "path": "test/fixtures/discussions.js",
    "content": "module.exports = {\n  \"next\":2,\n  \"results\":[\n    {\n      \"id\":595,\n      \"url\":\"/api/v2/discussion/595/\",\n      \"reply_count\":0,\n      \"intro\":\"blop\",\n      \"slug\":\"bleep-bloop\",\n      \"title\":\"bleep bloop\",\n      \"unread_count\":0,\n      \"team\":\"/api/v2/team/9/\",\n      \"message\":{\n        \"id\":3798,\n        \"url\":\"/api/v2/message/3798/\",\n        \"attachments\":[],\n        \"body\":\"<p>blop</p>\\n\",\n        \"raw_body\":\"blop\",\n        \"collapsed\":false,\n        \"date_created\":\"2013-09-13T06:00:40.421\",\n        \"date_edited\":null,\n        \"date_latest_activity\":\"2014-02-20T11:42:04.251\",\n        \"discussion\":\"/api/v2/discussion/595/\",\n        \"parent\":null,\n        \"root\":null,\n        \"permalink\":\"/test-sandbox/595/bleep-bloop/\",\n        \"read\":true,\n        \"user\":\"/api/v2/user/66238278d2ce670dcb448f96258dc732/\",\n        \"votes\":[\n          {\n            \"url\": \"/api/v2/message/3252/vote/60617b03-1663-4595-a90e-a8197fe668af/\",\n            \"user\": \"/api/v2/user/c17ec52b925fc0fe2f4eadf1f10b0bf7/\",\n            \"value\": \"+1\",\n            \"date_created\": \"2011-10-20T16:29:07.639\"\n          }\n        ]\n      }\n    },\n    {\n      \"id\":592,\n      \"url\":\"/api/v2/discussion/592/\",\n      \"reply_count\":0,\n      \"intro\":\"unicode\",\n      \"slug\":\"unicode\",\n      \"title\":\"unicode\",\n      \"unread_count\":0,\n      \"team\":\"/api/v2/team/9/\",\n      \"message\":{\n        \"id\":3789,\n        \"url\":\"/api/v2/message/3789/\",\n        \"attachments\":[\n          {\n            \"url\":\"/api/v2/attachment/29b38982-2bf0-41eb-94d5-872a344ef88d/\",\n            \"size\":null,\n            \"filename\":\"t\\u00e5st\\u00f6\\u00f3\\u00f8\\u00e4.txt\",\n            \"thumbnail\":null,\n            \"user\":\"/api/v2/user/c17ec52b925fc0fe2f4eadf1f10b0bf7/\",\n            \"message\":\"/api/v2/message/3789/\",\n            \"attachment\":\"/file/2013/06/04/t%C3%A5st%C3%B6%C3%B3%C3%B8%C3%A4.txt\",\n            \"created\":\"2013-06-04T08:07:24.920\"\n          }\n        ],\n        \"body\":\"<p>unicode</p>\\n\",\n        \"raw_body\":\"unicode\",\n        \"collapsed\":false,\n        \"date_created\":\"2013-06-04T08:07:26.678\",\n        \"date_edited\":null,\n        \"date_latest_activity\":\"2013-08-28T14:42:54.087\",\n        \"discussion\":\"/api/v2/discussion/592/\",\n        \"parent\":null,\n        \"root\":null,\n        \"permalink\":\"/test-sandbox/592/unicode/\",\n        \"read\":true,\n        \"user\":\"/api/v2/user/c17ec52b925fc0fe2f4eadf1f10b0bf7/\",\n        \"votes\":[]\n      }\n    },\n    {\n      \"id\":593,\n      \"url\":\"/api/v2/discussion/593/\",\n      \"reply_count\":0,\n      \"intro\":\"Can someone please mention me?\",\n      \"slug\":\"mention-me-please\",\n      \"title\":\"Mention me, please?\",\n      \"unread_count\":0,\n      \"team\":\"/api/v2/team/9/\",\n      \"message\":{\n        \"id\":3790,\n        \"url\":\"/api/v2/message/3790/\",\n        \"attachments\":[],\n        \"body\":\"<p>Can someone please mention me?</p>\\n\",\n        \"raw_body\":\"Can someone please mention me?\",\n        \"collapsed\":false,\n        \"date_created\":\"2013-07-03T16:09:34.351\",\n        \"date_edited\":null,\n        \"date_latest_activity\":\"2013-07-03T16:34:25.065\",\n        \"discussion\":\"/api/v2/discussion/593/\",\n        \"parent\":null,\n        \"root\":null,\n        \"permalink\":\"/test-sandbox/593/mention-me-please/\",\n        \"read\":true,\n        \"user\":\"/api/v2/user/f31abb30271cdecae75a6227128c8fd9/\",\n        \"votes\":[]\n      }\n    },\n    {\n      \"id\":590,\n      \"url\":\"/api/v2/discussion/590/\",\n      \"reply_count\":0,\n      \"intro\":\"Hey [@Yann Malet](user:/api/v2/user/e7c323f0011c91b78a8bd5e2d6df8d03/) how are yo?\",\n      \"slug\":\"unicode-mentions\",\n      \"title\":\"Unicode Mentions\",\n      \"unread_count\":0,\n      \"team\":\"/api/v2/team/9/\",\n      \"message\":{\n        \"id\":3779,\n        \"url\":\"/api/v2/message/3779/\",\n        \"attachments\":[],\n        \"body\":\"<p>Hey <span data-user=\\\"e7c323f0011c91b78a8bd5e2d6df8d03\\\" class=\\\"user-mention\\\">Yann Malet</span> how are yo?</p>\\n\",\n        \"raw_body\":\"Hey [@Yann Malet](user:/api/v2/user/e7c323f0011c91b78a8bd5e2d6df8d03/) how are yo?\",\n        \"collapsed\":false,\n        \"date_created\":\"2013-05-09T12:08:54.761\",\n        \"date_edited\":null,\n        \"date_latest_activity\":\"2013-05-09T13:08:49.379\",\n        \"discussion\":\"/api/v2/discussion/590/\",\n        \"parent\":null,\n        \"root\":null,\n        \"permalink\":\"/test-sandbox/590/unicode-mentions/\",\n        \"read\":true,\n        \"user\":\"/api/v2/user/66238278d2ce670dcb448f96258dc732/\",\n        \"votes\":[]\n      }\n    }\n  ]\n};\n"
  },
  {
    "path": "test/fixtures/teams.js",
    "content": "module.exports = [\n  {\n    \"url\":\"/api/v2/team/9/\",\n    \"name\":\"Test Sandbox\",\n    \"slug\":\"test-sandbox\",\n    \"description\":\"\",\n    \"unread\":0,\n    \"date_created\":\"2012-01-04T21:51:18.514\",\n    \"members\":[\n      {\n        \"is_owner\":false,\n        \"url\":\"/api/v2/team/9/member/66238278d2ce670dcb448f96258dc732/\",\n        \"date_updated\":\"2012-01-09T00:00:00\",\n        \"is_admin\":false,\n        \"user\":\"/api/v2/user/66238278d2ce670dcb448f96258dc732/\",\n        \"date_created\":\"2012-01-09T00:00:00\",\n        \"notify\":2\n      },\n      {\n        \"is_owner\":false,\n        \"url\":\"/api/v2/team/9/member/c17ec52b925fc0fe2f4eadf1f10b0bf7/\",\n        \"date_updated\":\"2012-01-09T00:00:00\",\n        \"is_admin\":false,\n        \"user\":\"/api/v2/user/c17ec52b925fc0fe2f4eadf1f10b0bf7/\",\n        \"date_created\":\"2012-01-09T00:00:00\",\n        \"notify\":2\n      },\n      {\n        \"is_owner\":true,\n        \"url\":\"/api/v2/team/9/member/f31abb30271cdecae75a6227128c8fd9/\",\n        \"date_updated\":\"2012-01-24T20:43:43.180\",\n        \"is_admin\":true,\n        \"user\":\"/api/v2/user/f31abb30271cdecae75a6227128c8fd9/\",\n        \"date_created\":\"2012-01-09T00:00:00\",\n        \"notify\":2\n      },\n    ],\n    \"channel_token\":\"dabe371eb42f42eb9b82ff471fd73d1b\",\n    \"organization\":\"Lincoln Loop\",\n    \"post_email\":\"test-sandbox@example.com\"\n  }\n];\n"
  },
  {
    "path": "test/fixtures/users.js",
    "content": "module.exports = [\n  {\n    \"url\":\"/api/v2/user/66238278d2ce670dcb448f96258dc732/\",\n    \"name\":\"Martin\",\n    \"email\":\"martin@mahner.org\"\n  },\n  {\n    \"url\": \"/api/v2/user/c17ec52b925fc0fe2f4eadf1f10b0bf7/\",\n    \"name\": \"Michael\",\n    \"email\": \"michael@lincolnloop.com\"\n  },\n  {\n    \"url\": \"/api/v2/user/f31abb30271cdecae75a6227128c8fd9/\",\n    \"name\": \"Brandon Konkle\",\n    \"email\": \"brandon@lincolnloop.com\"\n  }\n];\n"
  }
]