Full Code of lincolnloop/amygdala for AI

master f3bf5b109182 cached
14 files
54.7 KB
14.9k tokens
1 requests
Download .txt
Repository: lincolnloop/amygdala
Branch: master
Commit: f3bf5b109182
Files: 14
Total size: 54.7 KB

Directory structure:
gitextract_t3uznu7z/

├── .editorconfig
├── .gitignore
├── .jshintrc
├── LICENSE
├── README.md
├── amygdala.js
├── bower.json
├── gulpfile.js
├── package.json
├── templates/
│   └── umd.jst
└── test/
    ├── amygdala.test.js
    └── fixtures/
        ├── discussions.js
        ├── teams.js
        └── users.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# http://editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true


================================================
FILE: .gitignore
================================================
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz

pids
logs
results

npm-debug.log
node_modules

*.sublime-project
*.sublime-workspace

build


================================================
FILE: .jshintrc
================================================
{
  "bitwise": true,
  "curly": true,
  "eqeqeq": true,
  "esnext": true,
  "expr": true,
  "forin": false,
  "immed": true,
  "indent": 2,
  "latedef": true,
  "loopfunc": true,
  "newcap": false,
  "noarg": true,
  "quotmark": false,
  "regexp": true,
  "scripturl": true,
  "smarttabs": true,
  "strict": true,
  "sub": true,
  "trailing": true,
  "undef": true,
  "unused": true,

  // Predefined globals that JSHint will ignore.
  "node": true,
  "browser": true
}


================================================
FILE: LICENSE
================================================
Copyright (c) 2014, Lincoln Loop
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

================================================
FILE: README.md
================================================
![Amygdala logo](https://raw.githubusercontent.com/lincolnloop/amygdala/master/static/logo.png)

Amygdala 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.

Examples:

```javascript
// GET
store.get('users').done(function() { ... });

// POST
store.add('teams', {'name': 'Lincoln Loop', 'active': true}).done(function() { ... });
```

[![browser support](https://ci.testling.com/lincolnloop/amygdala.png)
](https://ci.testling.com/lincolnloop/amygdala)

## How it works

### 1. INSTALL


#### NPM/Browserify

`npm install amygdala`.


#### Bower

`bower install amygdala`


#### Browser

Download the latest [amygdala.js](https://github.com/lincolnloop/amygdala/blob/master/amygdala.js) file. Minified/build version coming soon.

##### Dependencies:

* [lodash](https://lodash.com): ^3.10.1
* [q](https://github.com/kriskowal/q): ^1.0.1
* [Wolfy87/EventEmitter](https://github.com/Wolfy87/EventEmitter): ^4.2.6


### 2. SETUP

To create a new store, define the few possible settings listed below and your API schema.

```javascript
var store = new Amygdala({
  'config': {
    'apiUrl': 'http://localhost:8000',
    'idAttribute': 'url',
    'headers': {
      'X-CSRFToken': getCookie('csrftoken')
    },
    'localStorage': true
  },
  'schema': {
    'users': {
      'url': '/api/v2/user/'
    },
    'teams': {
      'url': '/api/v2/team/',
      'orderBy': 'name',
      'oneToMany': {
        'members': 'members'
      },
      parse: function(data) {
        return data.results ? data.results : data;
      },
    },
    'members': {
      'foreignKey': {
        'user': 'users'
      }
    }
  }
});
```

#### Configuration options:

  * apiUrl - Full path to your base API url (required).
  * idAttribute - global primary key attribute (required). 
  * headers - Any headers that you need to pass on each API request.
  * localStorage - enable/disable the persistent localStorage cache.

#### Schema options:
  
  * url - relative path for each "table" (required)
  * orderBy - order by which you want to retrieve local cached data. eg (name, -name (for reverse))
  * parse - Accepts a parse method for cases when your API also returns extra meta data.
  * idAttribute - overrides key attribute (if different in this schema)


#### Schema relations:

When 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.

Consider the following schema, that defines discussions that have messages, and messages that have votes:

```javascript
var store = new Amygdala({
    'config': {
      'apiUrl': 'http://localhost:8000',
      'idAttribute': 'url'
    },
    'schema': {
      'discussions': {
        'url': '/api/v2/discussion/',
        'oneToMany': {
          'children': 'messages'
        }
      },
      'messages': {
        'url': '/api/v2/message/',
        'oneToMany': {
          'votes': 'votes'
        },
        'foreignKey': {
          'discussion': 'discussions'
        }
      },
      'votes': {
        'url': '/api/v2/vote/'
      }
    }
  }
);
```

In this scenario, doing a query on a discussion will retrieve all messages and votes for that discussion:

```javascript
store.get('discussions', {'url': '/api/v2/discussion/85273/'}).then(function(){ ... });
```

Since 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.

##### OneToMany:

```javascript
'oneToMany': {
  'children': 'messages'
}
```

`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.


##### foreignKey:

```javascript
'foreignKey': {
  'discussion': 'discussions'
}
```

`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.


### 3. USAGE

#### Querying the remote API server:

The methods below, allow you to make remote calls to your API server.

```javascript
// GET
store.get('users').done(function() { ... });

// POST
store.add('teams', {'name': 'Lincoln Loop', 'active': true}).done(function() { ... });

// PUT
store.update('users', {'url': '/api/v2/user/32/', 'username': 'amy82', 'active': true}).done(function() { ... });

// DELETE
store.remove('users', {'url': '/api/v2/user/32/'}).done(function() { ... });
```


#### In memory storage API:

On top of this, Amygdala also stores a copy of your data locally, which you can access through a couple different methods:


###### Find and filtering:

```javascript
// Get the list of active users from memory
var users = store.findAll('users', {'active': true});

// Get a single user from memory
var user = store.find('users', {'username': 'amy82'});

// Get a single user by id for memory
var user = store.find('users', 1103747470);
```

If 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)


##### Fetching related data:

By defining your schema and creating relations between data, you are then able to query your data objects for the related objects.

In 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.

```javascript
// Fetching related messages for a discussion (oneToMay)
var messages = store.find('discussions', '/api/v2/discussion/85273/').getRelated('messages');

// Getting the discussion object from a message (foreignKey)
var discussion = store.find('message', '/api/v2/message/81273/').getRelated('discussion');
```

Note that Amygdala doesn't fetch data automagically for you here, so it's up you to fetch it before running the query.

## Events

Amygdala uses [Wolfy87/EventEmitter](https://github.com/Wolfy87/EventEmitter) under the hood
to trigger some very basic events. Right now it only triggers two different events:

* change
* change:type

To 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`:

```javascript
// Listen to any change in the store
store.on('change', function() { ... });

// Listen to any change of a specific type
store.on('change:users', function() { ... });
```


================================================
FILE: amygdala.js
================================================
'use strict';

// CommonJS check so we can require dependencies
if (typeof module === 'object' && module.exports) {
  var each = require('lodash/collection/each');
  var partial = require('lodash/function/partial');
  var debounce = require('lodash/function/debounce');
  var clone = require('lodash/lang/clone');
  var isString = require('lodash/lang/isString');
  var isObject = require('lodash/lang/isObject');
  var isFunction = require('lodash/lang/isFunction');
  var isArray = require('lodash/lang/isArray');
  var isEmpty = require('lodash/lang/isEmpty');
  var defaults = require('lodash/object/defaults');
  var map = require('lodash/collection/map');
  var filter = require('lodash/collection/filter');
  var findWhere = require('lodash/collection/findWhere');
  var sortBy = require('lodash/collection/sortBy');
  var Q = require('q');
  var EventEmitter = require('wolfy87-eventemitter');
}

var Amygdala = function(options) {
  // Initialize a new Amygdala instance with the given schema and options.
  //
  // params:
  // - options (Object)
  //   - config (apiUrl, headers)
  //   - schema
  this._config = options.config;
  this._schema = options.schema;
  this._headers = this._config.headers;

  // if not apiUrl is defined, use current location origin
  if (!this._config.apiUrl) {
    this._config.apiUrl = window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
  }

  // memory data storage
  this._store = {};
  this._changeEvents = {};

  if (this._config.localStorage) {
    each(this._schema, function(value, key) {
      // check each schema entry for localStorage data
      // TODO: filter out apiUrl and idAttribute 
      var storageCache = window.localStorage.getItem('amy-' + key);
      if (storageCache) {
        this._set(key, JSON.parse(storageCache), {'silent': true} );
      }
    }.bind(this));

    // store every change on local storage
    // when localStorage is set to true
    this.on('change', function(type) {
      this.setCache(type, this.findAll(type));
    }.bind(this));
  }
};

Amygdala.prototype = clone(EventEmitter.prototype);

// ------------------------------
// Helper methods
// ------------------------------
Amygdala.prototype.serialize = function serialize(obj) {
  // Translates an object to a querystring

  if (!isObject(obj)) {
    return obj;
  }
  var pairs = [];
  each(obj, function(value, key) {
    pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
  });
  return pairs.join('&');
}

Amygdala.prototype.ajax = function ajax(method, url, options) {
   // Sends an Ajax request, converting the data into a querystring if the
   // method is GET.
   //
   // params:
   // -method (string): GET, POST, PUT, DELETE
   // -url (string): The url to send the request to.
   // -options (Object)
   //
   // options
   // - data (Object): Will be converted to a querystring for GET requests.
   // - contentType (string): A value for the Content-Type request header.
   // - headers (Object): Additional headers to add to the request.
  var query;
  options = options || {};

  if (!isEmpty(options.data) && method === 'GET') {
    query = this.serialize(options.data);
    url = url + '?' + query;
  }

  var request = new XMLHttpRequest();
  var deferred = Q.defer();

  request.open(method, url, true);

  request.onload = function() {
    // status 200 OK, 201 CREATED, 20* ALL OK
    if (request.status.toString().substr(0, 2) === '20') {
      deferred.resolve(request);
    } else {
      deferred.reject(request);
    }
  };

  request.onerror = function() {
    deferred.reject(new Error('Unabe to send request to ' + JSON.stringify(url)));
  };

  if (!isEmpty(options.contentType)) {
    request.setRequestHeader('Content-Type', options.contentType);
  }

  if (!isEmpty(options.headers)) {
    each(options.headers, function(value, key) {
      if (isFunction(value)) {
        request.setRequestHeader(key,value());
      }
      else {
        request.setRequestHeader(key, value);
      }
    });
  }

  request.send(method === 'GET' ? null : options.data);

  return deferred.promise;
}

// ------------------------------
// Internal utils methods
// ------------------------------
Amygdala.prototype._getURI = function(type, params) {
  var url;
  // get absolute uri for api endpoint
  if (!this._schema[type] || !this._schema[type].url) {
    throw new Error('Invalid type. Acceptable types are: ' + Object.keys(this._schema));
  }
  url = this._config.apiUrl + this._schema[type].url;

  // if the `idAttribute` specified by the schema or config
  // exists as a key in `params` append it's value to the url,
  // and remove it from `params` so it's not sent in the query string.
  if (params && this._getIdAttribute(type) in params) {
    url += params[this._getIdAttribute(type)];
    delete params[this._getIdAttribute(type)];
  }

  return url;
},

Amygdala.prototype._getIdAttribute = function(type) {
  // schema may override idAttribute
  return this._schema[type].idAttribute || this._config.idAttribute;
},

Amygdala.prototype._emitChange = function(type) {

  // TODO: Add tests for debounced events
  if (!this._changeEvents[type]) {
    this._changeEvents[type] = debounce(partial(function(type) {
      // emit changes events
      this.emit('change', type);
      // change:<type>
      this.emit('change:' + type);
      // TODO: compare the previous object and trigger change events
    }.bind(this), type), 150);
  }
  
  this._changeEvents[type]();
}

// ------------------------------
// Internal data sync methods
// ------------------------------
Amygdala.prototype._set = function(type, response, options) {
  // Adds or Updates an item of `type` in this._store.
  //
  // type: schema key/store (teams, users)
  // ajaxResponse: response to store in local cache

  // initialize store for this type (if needed)
  // and store it under `store` for easy access.
  var store = this._store[type] ? this._store[type] : this._store[type] = {};
  var schema = this._schema[type];
  var wrappedResponse = false;

  if (isString(response)) {
    // If the response is a string, try JSON.parse.
    try {
      response = JSON.parse(response);
    } catch(e) {
      throw('Invalid JSON from the API response.');
    }
  }

  if (!isArray(response)) {
    // The response isn't an array. We need to figure out how to handle it.
    if (schema.parse) {
      // Prefer the schema's parse method if one exists.
      response = schema.parse(response);
      // if it's still not an array, wrap it around one
      if (!isArray(response)) {
        response = [response];
      }
    } else {
      // Otherwise, just wrap it in an array and hope for the best.
      response = [response];
      wrappedResponse = true;
    }
  }

  each(response, function(obj) {
    // store the object under this._store['type']['id']
    store[obj[this._getIdAttribute(type)]] = obj;

    // handle oneToMany relations
    each(this._schema[type].oneToMany, function(relatedType, relatedAttr) {
      var related = obj[relatedAttr];
      // check if obj has a `relatedAttr` that is defined as a relation
      if (related) {
        // check if attr value is an array,
        // if it's not empty, and if the content is an object and not a string
        if (Object.prototype.toString.call(related) === '[object Array]' &&
          related.length > 0 &&
          Object.prototype.toString.call(related[0]) === '[object Object]') {
          // if related is a list of objects,
          // populate the relation `table` with this data
          this._set(relatedType, related);
          // and replace the list of objects within `obj`
          // by a list of `id's
          obj[relatedAttr] = map(related, function(item) {
            return item[this._getIdAttribute(type)];
          }.bind(this));
        }
      }
    }.bind(this));

    // handle foreignKey relations
    each(this._schema[type].foreignKey, function(relatedType, relatedAttr) {
      var related = obj[relatedAttr];
      // check if obj has a `relatedAttr` that is defined as a relation
      if (related) {
        // check if `obj[relatedAttr]` value is an object (FK should not be arrays),
        // if it's not empty, and if the content is an object and not a string
        if (Object.prototype.toString.call(related) === '[object Object]') {
          // if related is an object,
          // populate the relation `table` with this data
          this._set(relatedType, [related]);
          // and replace the list of objects within `item`
          // by a list of `id's
          obj[relatedAttr] = related[this._getIdAttribute(type)];
        }
      }
    }.bind(this));

    // obj.related()
    // set up a related method to fetch other related objects
    // as defined in the schema for the store.
    obj.getRelated = partial(function(schema, obj, attributeName) {
      if (schema.oneToMany && attributeName in schema.oneToMany) {
        //
        // if oneToMany relation
        //
        // loop through each id in the obj
        // and return the full related object list as the response
        return obj[attributeName].map(function(value) {
          // find in related `table` by id
          return this.find(schema.oneToMany[attributeName], value);
        }.bind(this)).filter(function(value) {
          // filter out undefined/null values
          return !!value;
        });
      } else if (schema.foreignKey && attributeName in schema.foreignKey) {
        //
        // else, if foreignKey relation
        //
        //
        // find in related `table` by id
        return this.find(schema.foreignKey[attributeName], obj[attributeName]);
      }
      return null;
    }.bind(this), schema, obj);

    // emit change events
    if (!options || options.silent !== true) {
      this._emitChange(type);
    }

  }.bind(this));

  // return our data as the original api call's response
  return wrappedResponse && response.length === 1 ? response[0] : response;
};

Amygdala.prototype._setAjax = function(type, request, options) {
  return this._set(type, request.response, options);
}

Amygdala.prototype._remove = function(type, object) {
  // Removes an item of `type` from this._store.
  //
  // type: schema key/store (teams, users)
  // response: response to store in local cache

  this._emitChange(type);

  // delete object of type by id
  delete this._store[type][object[this._getIdAttribute(type)]]
};

Amygdala.prototype._validateURI = function(url) {
  // convert paths to full URLs
  // TODO: DRY UP
  if (url.indexOf('/') === 0) {
    return this._config.apiUrl + url;
  }

  return url;
}

// ------------------------------
// Public data sync methods
// ------------------------------
Amygdala.prototype._get = function(url, params) {
  // AJAX post request wrapper
  // TODO: make this method public in the future

  // Request settings
  var settings = {
    'data': params,
    'headers': this._headers
  };

  return this.ajax('GET', this._validateURI(url), settings);
}

Amygdala.prototype.get = function(type, params, options) {
  // GET request for `type` with optional `params`
  //
  // type: schema key/store (teams, users)
  // params: extra queryString params (?team=xpto&user=xyz)
  // options: extra options
  // - url: url override

  // Default to the URI for 'type'
  options = options || {};
  defaults(options, {'url': this._getURI(type, params)});

  return this._get(options.url, params)
    .then(partial(this._setAjax, type).bind(this));
};

Amygdala.prototype._post = function(url, data) {
  // AJAX post request wrapper
  // TODO: make this method public in the future

  // Request settings
  var settings = {
    'data': data ? JSON.stringify(data) : null,
    'contentType': 'application/json',
    'headers': this._headers
  };

  return this.ajax('POST', this._validateURI(url), settings);
}

Amygdala.prototype.add = function(type, object, options) {
  // POST/PUT request for `object` in `type`
  //
  // type: schema key/store (teams, users)
  // object: object to update local and remote
  // options: extra options
  // -  url: url override

  // Default to the URI for 'type'
  options = options || {};
  defaults(options, {'url': this._getURI(type)});

  // Dynamic URL is now accepted in post
  object.url ? options.url = object.url : null;
  
  return this._post(options.url, object)
    .then(partial(this._setAjax, type).bind(this));
};

Amygdala.prototype._put = function(url, data) {
  // AJAX put request wrapper
  // TODO: make this method public in the future

  // Request settings
  var settings = {
    'data': JSON.stringify(data),
    'contentType': 'application/json',
    'headers': this._headers
  };

  return this.ajax('PUT', this._validateURI(url), settings);
}

Amygdala.prototype.update = function(type, object) {
  // POST/PUT request for `object` in `type`
  //
  // type: schema key/store (teams, users)
  // object: object to update local and remote
  var url = object.url;

  if (!url && this._getIdAttribute(type) in object) {
    url = this._getURI(type, object);
  }

  if (!url) {
    throw new Error('Missing required object.url or ' + this._getIdAttribute(type) + ' attribute.');
  }

  return this._put(url, object)
    .then(partial(this._setAjax, type).bind(this));
};

Amygdala.prototype._delete = function(url, data) {
  // AJAX delete request wrapper
  // TODO: make this method public in the future
  var settings = {
    'data': JSON.stringify(data),
    'contentType': 'application/json',
    'headers': this._headers
  };

  return this.ajax('DELETE', this._validateURI(url), settings);
}

Amygdala.prototype.remove = function(type, object) {
  // DELETE request for `object` in `type`
  //
  // type: schema key/store (teams, users)
  // object: object to update local and remote
  var url = object.url;

  if (!url && this._getIdAttribute(type) in object) {
    url = this._getURI(type, object);
  }

  if (!url) {
    throw new Error('Missing required object.url or ' + this._getIdAttribute(type) + ' attribute.');
  }

  return this._delete(url, object)
    .then(partial(this._remove, type, object).bind(this));
};

// ------------------------------
// Public cache methods
// ------------------------------
Amygdala.prototype.setCache = function(type, objects) {
  if (!type) {
    throw new Error('Missing schema type parameter.');
  }
  if (!this._schema[type]) {
    throw new Error('Invalid type. Acceptable types are: ' + Object.keys(this._schema));
  }
  return window.localStorage.setItem('amy-' + type, JSON.stringify(objects));
};

Amygdala.prototype.getCache = function(type) {
  if (!type) {
    throw new Error('Missing schema type parameter.');
  }
  if (!this._schema[type] || !this._schema[type].url) {
    throw new Error('Invalid type. Acceptable types are: ' + Object.keys(this._schema));
  }
  return JSON.parse(window.localStorage.getItem('amy-' + type));
};

// ------------------------------
// Public query methods
// ------------------------------
Amygdala.prototype.findAll = function(type, query) {
  // find a list of items within the store. (THAT ARE NOT STORED IN BACKBONE COLLECTIONS)
  var store = this._store[type];
  var orderBy;
  var reverseMatch;
  var results;
  if (!store || !Object.keys(store).length) {
    return [];
  }
  if (query === undefined) {
    // query is empty, no object is returned
    results = map(store, function(item) { return item; });
  } else if (Object.prototype.toString.call(query) === '[object Object]') {
    // if query is an object, assume it specifies filters.
    results = filter(store, function(item) { return findWhere([item], query); });
  } else {
    throw new Error('Invalid query for findAll.');
  }
  orderBy = this._schema[type].orderBy;
  if (orderBy) {
    // match the orderBy attribute for the presence
    // of a reverse flag
    reverseMatch = orderBy.match(/^-([\w-]{0,})$/);
    if (reverseMatch !== null) {
      // if we have two matches, we have a reverse flag
      orderBy = orderBy.replace('-', '');
    }
    results = sortBy(results, function(item) {
      return item[orderBy].toString().toLowerCase();
    }.bind(this));

    if (reverseMatch !== null) {
      // reverse the results
      results = results.reverse();
    }
  }
  return results;
};

Amygdala.prototype.find = function(type, query) {
  // find a specific within the store. (THAT ARE NOT STORED IN BACKBONE COLLECTIONS)
  var store = this._store[type];
  if (!store || !Object.keys(store).length) {
    return undefined;
  }
  if (query === undefined) {
    // query is empty, no object is returned
    return  undefined;
  } else if (Object.prototype.toString.call(query) === '[object Object]') {
    // if query is an object, return the first match for the query
    return findWhere(store, query);
  } else {
    // if query is a String or Number, assume it stores the key/url value
    // Object.prototype.toString.call(query) === '[object String]'
    // Object.prototype.toString.call(query) === '[object Number]'
    return store[query];
  }
};

// expose via CommonJS, AMD or as a global object
if (typeof module === 'object' && module.exports) {
  module.exports = Amygdala;
} else if (typeof define === 'function' && define.amd) {
  define(function() {
    return Amygdala;
  });
} else {
  window.Amygdala = Amygdala;
}


================================================
FILE: bower.json
================================================
{
  "name": "Amygdala",
  "main": "amygdala.js",
  "version": "0.4.5",
  "homepage": "https://github.com/lincolnloop/amygdala",
  "authors": [
    "Marco Louro <marco@lincolnloop.com> (http://lincolnloop.com)"
  ],
  "description": "RESTful HTTP library for JavaScript powered web applications",
  "keywords": [
    "REST",
    "client",
    "http",
    "API",
    "localStorage",
    "store",
    "browser",
    "library",
    "cache",
    "ajax",
    "offline"
  ],
  "license": "BSD",
  "dependencies": {
    "underscore"  : ">=1.6.0",
    "q": ">=1.0.1",
    "eventEmitter": ">=4.2.6"
  }
}


================================================
FILE: gulpfile.js
================================================
'use strict';

var _ = require('lodash');
var browserify = require('gulp-browserify');
var gulp = require('gulp');
var pkg = require('./package.json');
var rename = require('gulp-rename');
var uglify = require('gulp-uglify');
var wrap = require('gulp-wrap');

gulp.task('default', function() {
  // Set the environment to production
  process.env.NODE_ENV = 'production';
  gulp.start('distribute');
});

gulp.task('build', function() {
  var production = process.env.NODE_ENV === 'production';

  var stream = gulp.src('./amygdala.js')

    // Browserify, and add source maps if this isn't a production build
    .pipe(browserify({debug: !production}))

    .on('prebundle', function(bundler) {
      if (production) {
        // Externalize dependencies so they aren't included in the build
        bundler.external('underscore');
        bundler.external('q');

        // Export Amygdala as 'amygdala'
        bundler.require('./amygdala.js', {expose: 'amygdala'});
      }
    })

    // Rename the destination file
    .pipe(rename(pkg.name + '.js'));

  if (production) {
    // Wrap in a UMD template
    stream.pipe(wrap({src: 'templates/umd.jst'}, {
      pkg: pkg,
      namespace: 'Amygdala',
      deps: {
        'underscore': '_',
        'q': 'Q'
      },
      expose: 'amygdala'
    }, {'imports': {'_': _}}));
  }

  // Dist directory if production, otherwise the ignored build dir
  stream.pipe(gulp.dest(production ? 'dist/' : 'build/'));

  return stream;
});

gulp.task('distribute', ['build'], function() {
  gulp.src('dist/' + pkg.name + '.js')
    .pipe(uglify())
    .pipe(rename(pkg.name + '.min.js'))
    .pipe(gulp.dest('dist/'));
});


================================================
FILE: package.json
================================================
{
  "name": "amygdala",
  "version": "0.5.0",
  "description": "RESTful HTTP library for JavaScript powered web applications",
  "keywords": [
    "REST",
    "client",
    "http",
    "API",
    "localStorage",
    "store",
    "browser",
    "library",
    "cache",
    "ajax",
    "offline"
  ],
  "homepage": "https://github.com/lincolnloop/amygdala",
  "bugs": {
    "url": "https://github.com/lincolnloop/amygdala/issues"
  },
  "author": "Marco Louro <marco@lincolnloop.com> (http://lincolnloop.com)",
  "repository": {
    "type": "git",
    "url": "git://github.com/lincolnloop/amygdala.git"
  },
  "main": "amygdala.js",
  "files": [
    "amygdala.js"
  ],
  "scripts": {
    "test": "mocha --reporter spec --ui bdd"
  },
  "testling": {
    "harness": "mocha-bdd",
    "files": "test/*.test.js",
    "browsers": [
      "ie/9..latest",
      "chrome/26..latest",
      "firefox/22..latest",
      "safari/latest",
      "opera/12.0..latest",
      "iphone/latest",
      "ipad/latest",
      "android-browser/latest"
    ]
  },
  "licenses": [
    {
      "type": "BSD",
      "url": "http://github.com/lincolnloop/amygdala/blob/master/LICENSE"
    }
  ],
  "devDependencies": {
    "chai": "^1.9.0",
    "mocha": "^1.17.1",
    "sinon": "^1.9.0",
    "sinon-chai": "^2.5.0",
    "xmlhttprequest": "^1.6.0",
    "gulp-rename": "^1.2.0",
    "gulp-wrap": "^0.3.0",
    "gulp": "^3.5.6",
    "gulp-browserify": "^0.5.0",
    "gulp-uglify": "^0.2.1"
  },
  "dependencies": {
    "lodash": "^3.10.0",
    "q": "^1.0.1",
    "wolfy87-eventemitter": "^4.2.6"
  }
}


================================================
FILE: templates/umd.jst
================================================
/*
 * Amygdala v<%= pkg.version %>
 * (c) <%= new Date().getFullYear() %> <%= pkg.author %>
 * <%= pkg.homepage %>
 * Licensed under the <%= pkg.licenses[0].type %> license.
 * <%= pkg.licenses[0].url %>
 */
<% var commonDeps = _.map(_.keys(deps), function(dep) {return "require('" + dep + "')";}); %>
<% var globalDeps = _.map(_.values(deps), function(dep) {return "root." + dep;}); %>
<% var requireMap = _.map(_.pairs(deps), function(dep) {return dep[0] + ': ' + dep[1];}); %>
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define(<%= _.keys(deps).join(', ') %>factory);
  } else if (typeof exports === 'object') {
    // Node. Does not work with strict CommonJS, but
    // only CommonJS-like environments that support module.exports,
    // like Node.
    module.exports = factory(<%= commonDeps.join(', ') %>);
  } else {
    // Browser globals (root is window)
    root.<%= namespace %> = factory(<%= globalDeps.join(', ') %>);
  }
}(this, function (<%= _.values(deps).join(', ') %>) {

  // A shim for 'require' so that it will work universally for externals
  var require = function(name) {
    return {<%= requireMap.join(', ') %>}[name];
  };

/*
 * -------- Begin module --------
 */
<%= contents %>
/*
 * -------- End module --------
 */

  return require('<%= expose %>');

}));


================================================
FILE: test/amygdala.test.js
================================================
/* global describe, it, before, after, beforeEach */
'use strict';

var _ = require('lodash');
var chai = require('chai');
var sinon = require('sinon');
var sinonChai = require('sinon-chai');
var Amygdala = require('../amygdala');

// fixtures
var teamFixtures = require('./fixtures/teams');
var userFixtures = require('./fixtures/users');
var discussionFixtures = require('./fixtures/discussions');

// Setup
var expect = chai.expect;
chai.use(sinonChai);
// log.setLevel('debug');

describe('Amygdala', function() {

  var store, authStore, xhr;
  var settings = {
    'config': {
      'apiUrl': 'http://localhost:8000',
      'idAttribute': 'url'
    },
    'schema': {
      'teams': {
        'url': '/api/v2/team/',
        'oneToMany': {
          'members': 'members'
        }
      },
      'users': {
        'url': '/api/v2/user/'
      },
      'members': {
        'foreignKey': {
          'user': 'users'
        }
      },
      'attachments': {
        'url': '/api/v2/attachment/',
        'foreignKey': {
          'user': 'users',
          'message': 'messages'
        }
      },
      'discussions': {
        'url': '/api/v2/discussion/',
        'orderBy': 'title',
        'foreignKey': {
          'message': 'messages',
          'team': 'teams'
        },
        parse: function(data) {
          return data.results;
        },
      },
      'messages': {
        'url': '/api/v2/message/',
        'oneToMany': {
          'attachments': 'attachments',
          'votes': 'votes'
        },
        'foreignKey': {
          'user': 'users',
          'discussion': 'discussions'
        }
      },
      'votes': {
        'url': '/api/v2/vote/',
        'foreignKey': {
          'message': 'messages'
        }
      }
    }
  };

  before(function() {
    store = new Amygdala(settings);
    store._set('users', userFixtures);
    store._set('teams', teamFixtures);
    store._set('discussions', discussionFixtures);

    var authSettings = {
      'config': {
        'apiUrl': 'http://localhost:8000',
        'idAttribute': 'url',
        'headers': {'Authorization': 'alpha'}
      },
      'schema': settings.schema
    };

    authStore = new Amygdala(authSettings);
    authStore._set('users', userFixtures);
    authStore._set('teams', teamFixtures);
    authStore._set('discussions', discussionFixtures);

    global.XMLHttpRequest = function() {
      return xhr;
    };
  });

  beforeEach(function() {
    // Reset the xhr spy between each test
    xhr = {
      'open': sinon.spy(),
      'send': sinon.spy(),
      'setRequestHeader': sinon.spy()
    };
  });

  after(function() {
    delete global.XMLHttpRequest;
  });

  describe('#_set()', function() {

    it('loads simple models correctly', function() {
      expect(Object.keys(store._store['teams'])).to.have.length(1);
    });

    it('populates tables based on one-to-many relations', function() {
      expect(Object.keys(store._store['members'])).to.have.length(3);
    });

    it('replaces objects by id\'s in one-to-many relations', function() {
      expect(
        store._store.teams['/api/v2/team/9/'].members
          .indexOf('/api/v2/team/9/member/f31abb30271cdecae75a6227128c8fd9/')
      ).to.not.equal(-1);
    });

    it('replaces objects by id\'s in foreign-key relations', function() {
      expect(
        store._store.discussions['/api/v2/discussion/595/'].message
      ).to.equal('/api/v2/message/3798/');
    });

    it('sets up a helper method to get related objects', function() {
      expect(store.find('teams', '/api/v2/team/9/').getRelated).to.exist;
    });

    it('uses data parsers when they are defined', function() {
      // discussions have a non-standard data structure due to pagintation,
      // so the schema provides a `parse` method.
      // for storage purposes we only want the objects, not the meta data.
      expect(Object.keys(store._store['discussions'])).to.have.length(4);
    });

    it('attempts to parse JSON if the format of the response is a string', function() {
      // Create an empty store for this test
      var jsonStore = new Amygdala(settings);

      // Set the users with a JSON string
      jsonStore._set('users', JSON.stringify(userFixtures));

      expect(Object.keys(jsonStore._store['users'])).to.have.length(3);
      expect(_.pluck(jsonStore._store['users'], 'name')).to.contain('Brandon Konkle');
    });

    it('Single item result lists are returned correctly', function() {
      // Create an empty store for this test
      var jsonStore = new Amygdala(settings);

      // Set the users with a JSON string
      jsonStore._set('users', [userFixtures[0]]);

      expect(Object.keys(jsonStore._store['users'])).to.have.length(1);
      expect(_.pluck(jsonStore._store['users'], 'name')).to.contain('Martin');
    });

    it('Single item result is returned correctly', function() {
      // Create an empty store for this test
      var jsonStore = new Amygdala(settings);

      // Set the users with a JSON string
      jsonStore._set('users', userFixtures[0]);
      expect(Object.keys(jsonStore._store['users'])).to.have.length(1);
      
      jsonStore._set('users', userFixtures[1]);
      expect(Object.keys(jsonStore._store['users'])).to.have.length(2);
      
      expect(_.pluck(jsonStore._store['users'], 'name')).to.contain('Martin');
    });

    it('will throw an error including the string if the JSON parse fails', function() {
      // Create an empty store for this test
      var jsonStore = new Amygdala(settings);

      // Set the users with an invalid string
      var invalidSet = function() {
        jsonStore._set('users', 'JSON fiesta!');
      };

      expect(invalidSet).to.throw('Invalid JSON from the API response.');
    });

  });

  describe('#<obj>.getRelated(<attributeName>)', function() {

    it('returns a list of objects based on the oneToMany relation', function() {
      expect(store.find('messages', '/api/v2/message/3798/').getRelated('votes')).to.have.length(1);
    });

    it('returns an object for foreignKey relations', function() {
      expect(store.find('messages', '/api/v2/message/3789/').getRelated('discussion').title)
        .to.equal('unicode');
    });

  });

  describe('#get()', function() {

    it('triggers an Ajax GET request', function() {
      store.get('discussions', {'id': 1});

      expect(xhr.open).to.have.been.calledOnce;
      expect(xhr.open).to.have.been.calledWith('GET', 'http://localhost:8000/api/v2/discussion/?id=1', true);
      expect(xhr.send).to.have.been.calledOnce;
      expect(xhr.send).to.have.been.calledWith(null);
    });

    it('calls #_set() with the given type', function(done) {
      var originalSet = store._set;
      store._set = sinon.spy();

      store.get('discussions', {'id': 1})
        .then(function() {
          expect(store._set).to.have.been.calledWith('discussions', 'response');

          // Clean up
          store._set = originalSet;
          done();
        }).catch(function(error) {
          // Catch and report errors
          done(error);
        });

      // Set the status, then trigger the resolution of the promise so the
      // 'then' block above is executed.
      xhr.status = 200;
      xhr.response = 'response';
      xhr.onload();
    });

    it('doesn\'t add any headers by default', function() {
      store.get('discussions', {'id': 1});

      expect(xhr.setRequestHeader).to.not.have.been.called;
    });

    it('will add headers if Amygdala was initialized with some', function() {
      authStore.get('discussions', {'id': 1});

      expect(xhr.setRequestHeader).to.have.been.calledOnce
        .and.have.been.calledWith('Authorization', 'alpha');
    });

  });

  describe('#add()', function() {
    var obj = {'name': 'The Alliance'};

    it('triggers an Ajax POST request', function() {
      store.add('teams', obj);

      expect(xhr.open).to.have.been.calledOnce
        .and.have.been.calledWith('POST', 'http://localhost:8000/api/v2/team/', true);
      expect(xhr.send).to.have.been.calledOnce
        .and.have.been.calledWith(JSON.stringify(obj));
    });

    it('calls #_set() with the given type', function(done) {
      var originalSet = store._set;
      store._set = sinon.spy();

      store.add('teams', obj)
        .then(function() {
          expect(store._set).to.have.been.calledWith('teams', 'response');

          // Clean up
          store._set = originalSet;
          done();
        }).catch(function(error) {
          // Catch and report errors
          done(error);
        });

      // Set the status, then trigger the resolution of the promise so the
      // 'then' block above is executed.
      xhr.status = 200;
      xhr.response = 'response';
      xhr.onload();
    });

    it('only adds the Content-Type header by deault', function() {
      store.add('teams', obj);

      expect(xhr.setRequestHeader).to.have.been.calledOnce
        .and.have.been.calledWith('Content-Type', 'application/json');
    });

    it('will add headers if Amygdala was initialized with some', function() {
      authStore.add('teams', obj);

      expect(xhr.setRequestHeader).to.have.been.calledTwice
        .and.have.been.calledWith('Authorization', 'alpha');
    });
    var obj2 = {'url': '/api/v2/team/anotherURL','name': 'The Alliance'};

	  it('triggers an Ajax POST request with custom url', function() {
		  store.add('teams', obj2);

		  expect(xhr.open).to.have.been.calledOnce
		    .and.have.been.calledWith('POST', 'http://localhost:8000/api/v2/team/anotherURL', true);
		  expect(xhr.send).to.have.been.calledOnce
		    .and.have.been.calledWith(JSON.stringify(obj2));
	  });

  });

  describe('#update()', function() {
    var obj = {'title': 'Rise of the Horde', 'url': '/draenor/'};

    it('triggers an Ajax PUT request', function() {

      store.update('messages', obj);

      expect(xhr.open).to.have.been.calledOnce;
      expect(xhr.open).to.have.been.calledWith('PUT', 'http://localhost:8000/draenor/', true);
      expect(xhr.send).to.have.been.calledOnce;
      expect(xhr.send).to.have.been.calledWith(JSON.stringify(obj));
    });

    it('calls #_set() with the given type', function(done) {
      var originalSet = store._set;
      store._set = sinon.spy();

      store.update('messages', obj)
        .then(function() {
          expect(store._set).to.have.been.calledWith('messages', 'response');

          // Clean up
          store._set = originalSet;
          done();
        }).catch(function(error) {
          // Catch and report errors
          done(error);
        });

      // Set the status, then trigger the resolution of the promise so the
      // 'then' block above is executed.
      xhr.status = 200;
      xhr.response = 'response';
      xhr.onload();
    });

    it('only adds the Content-Type header by deault', function() {
      store.update('messages', obj);

      expect(xhr.setRequestHeader).to.have.been.calledOnce
        .and.have.been.calledWith('Content-Type', 'application/json');
    });

    it('will add headers if Amygdala was initialized with some', function() {
      authStore.update('messages', obj);

      expect(xhr.setRequestHeader).to.have.been.calledTwice
        .and.have.been.calledWith('Authorization', 'alpha');
    });

  });

  describe('#remove()', function() {
    var obj = discussionFixtures.results[0];

    it('triggers an Ajax DELETE request', function() {
      store.remove('discussions', obj);

      expect(xhr.open).to.have.been.calledOnce;
      expect(xhr.open).to.have.been.calledWith('DELETE', 'http://localhost:8000/api/v2/discussion/595/', true);
      expect(xhr.send).to.have.been.calledOnce;
      expect(xhr.send).to.have.been.calledWith(JSON.stringify(obj));
    });

    it('calls #_remove() with the given type', function(done) {
      var originalRemove = store._remove;
      store._remove = sinon.spy();

      store.remove('discussions', obj)
        .then(function() {
          expect(store._remove).to.have.been.calledWith('discussions', obj);
          // Clean up
          store._remove = originalRemove;
          done();
        }).catch(function(error) {
          // Catch and report errors
          done(error);
        });

      // Set the status, then trigger the resolution of the promise so the
      // 'then' block above is executed.
      xhr.status = 200;
      xhr.response = 'response';
      xhr.onload();
    });

    it('removes the object from the cached store', function(done) {

      store.remove('discussions', obj)
        .then(function() {
          expect(store.find('discussions', obj.url)).to.equal(undefined);
          // add the discussion back into the store
          store._set('discussions', discussionFixtures);
          done();
        }).catch(function(error) {
          // Catch and report errors
          done(error);
        });

      // Set the status, then trigger the resolution of the promise so the
      // 'then' block above is executed.
      xhr.status = 200;
      xhr.response = 'response';
      xhr.onload();
    });

    it('only adds the Content-Type header by deault', function() {
      store.remove('discussions', obj);

      expect(xhr.setRequestHeader).to.have.been.calledOnce
        .and.have.been.calledWith('Content-Type', 'application/json');
    });

    it('will add headers if Amygdala was initialized with some', function() {
      authStore.remove('discussions', obj);

      expect(xhr.setRequestHeader).to.have.been.calledTwice
        .and.have.been.calledWith('Authorization', 'alpha');
    });

  });

  describe('#findAll()', function() {

    it('can find a list of type', function() {
      expect(store.findAll('discussions'))
        .to.have.length(4);
    });

    it('can find a list of type with filters', function() {
      expect(store.findAll('discussions', {'intro': 'unicode'}))
        .to.have.length(1);
    });

  });

  describe('#find()', function() {

    it('can find an object by id', function() {
      expect(store.find('teams', '/api/v2/team/9/').name)
        .to.equal('Test Sandbox');
    });

    it('can find an object with filters', function() {
      expect(store.find('discussions', {'intro': 'unicode'}).title)
        .to.equal('unicode');
    });

  });

  describe('#orderBy', function() {

    it('can sort', function() {
      expect(store.findAll('discussions')[0].title)
        .to.equal('bleep bloop');
    });

    it('can reverse sort', function() {
      expect(store.find('discussions', {'intro': 'unicode'}).title)
        .to.equal('unicode');
    });

  });

  describe('custom idAttribute', function() {
    // clone the settings, so changes don't affect the other tests
    var customSettings = _.clone(settings);
    // make sure we clone config too, so it does not override
    // the default config for the other tests.
    customSettings.config = _.extend(_.clone(customSettings.config), {'idAttribute': 'id'});
    // instatiate the customStore
    var customStore = new Amygdala(customSettings);

    it('is sent on GET requests as part of the url and not as a query string', function() {
      customStore.get('teams', {'id': 31});

      expect(xhr.open).to.have.been.calledOnce
        .and.have.been.calledWith('GET', 'http://localhost:8000/api/v2/team/31', true);
    });

    it('is set on PUT requests as part of the url and not as data', function() {
      customStore.update('teams', {'id': 31});

      expect(xhr.open).to.have.been.calledOnce
        .and.have.been.calledWith('PUT', 'http://localhost:8000/api/v2/team/31', true);
    });

    it('is set on DELETE requests as part of the url and not as data', function() {
      customStore.remove('teams', {'id': 31});

      expect(xhr.open).to.have.been.calledOnce
        .and.have.been.calledWith('DELETE', 'http://localhost:8000/api/v2/team/31', true);
    });

  });

  /*
  describe('^events', function() {

    it('triggers a change:<type> event when an object of <type> is added', function() {
      var callback = sinon.spy();
      // register the event
      store.on('change:teams', callback);
      // trigger the event on add
      store._set('teams', {'name': 'The <type> Event Team'});

      expect(callback).to.have.been.calledOnce;
    });

    it('triggers a change event when an object is changed', function() {
      var callback = sinon.spy();
      // register the event
      store.on('change', callback);
      // trigger the event on add
      store._set('teams', {
        'url': '/api/v2/team/9/',
        'name': 'The Event Team'
      });

      expect(callback).to.have.been.calledOnce;
    });

    it('triggers one change event for multiple changes of the same type', function() {
      var callback = sinon.spy();
      // register the event
      store.on('change', callback);
      // trigger the event on add
      store._set('teams', {
        'url': '/api/v2/team/9/',
        'name': 'The Event Team'
      });
      store._set('teams', {
        'url': '/api/v2/team/10/',
        'name': 'Zee Loop'
      });

      expect(callback).to.have.been.calledOnce;
    });

    it('triggers two events for changes in different types', function() {
      var callback = sinon.spy();
      // register the event
      store.on('change', callback);
      // trigger the event on add
      store._set('teams', {
        'url': '/api/v2/team/9/',
        'name': 'The Event Team'
      });
      store._set('users', {
        'url': '/api/v2/user/10/',
        'name': 'Me Robot'
      });

      expect(callback).to.have.been.calledTwice;
    });

    it('triggers a change event when an object is deleted', function() {
      var callback = sinon.spy();
      // register the event
      store.on('change', callback);
      // trigger the event on add
      store._remove('teams', {
        'url': '/api/v2/team/9/',
        'name': 'The Event Team'
      });

      expect(callback).to.have.been.calledOnce;
    });

  });
  */

});


================================================
FILE: test/fixtures/discussions.js
================================================
module.exports = {
  "next":2,
  "results":[
    {
      "id":595,
      "url":"/api/v2/discussion/595/",
      "reply_count":0,
      "intro":"blop",
      "slug":"bleep-bloop",
      "title":"bleep bloop",
      "unread_count":0,
      "team":"/api/v2/team/9/",
      "message":{
        "id":3798,
        "url":"/api/v2/message/3798/",
        "attachments":[],
        "body":"<p>blop</p>\n",
        "raw_body":"blop",
        "collapsed":false,
        "date_created":"2013-09-13T06:00:40.421",
        "date_edited":null,
        "date_latest_activity":"2014-02-20T11:42:04.251",
        "discussion":"/api/v2/discussion/595/",
        "parent":null,
        "root":null,
        "permalink":"/test-sandbox/595/bleep-bloop/",
        "read":true,
        "user":"/api/v2/user/66238278d2ce670dcb448f96258dc732/",
        "votes":[
          {
            "url": "/api/v2/message/3252/vote/60617b03-1663-4595-a90e-a8197fe668af/",
            "user": "/api/v2/user/c17ec52b925fc0fe2f4eadf1f10b0bf7/",
            "value": "+1",
            "date_created": "2011-10-20T16:29:07.639"
          }
        ]
      }
    },
    {
      "id":592,
      "url":"/api/v2/discussion/592/",
      "reply_count":0,
      "intro":"unicode",
      "slug":"unicode",
      "title":"unicode",
      "unread_count":0,
      "team":"/api/v2/team/9/",
      "message":{
        "id":3789,
        "url":"/api/v2/message/3789/",
        "attachments":[
          {
            "url":"/api/v2/attachment/29b38982-2bf0-41eb-94d5-872a344ef88d/",
            "size":null,
            "filename":"t\u00e5st\u00f6\u00f3\u00f8\u00e4.txt",
            "thumbnail":null,
            "user":"/api/v2/user/c17ec52b925fc0fe2f4eadf1f10b0bf7/",
            "message":"/api/v2/message/3789/",
            "attachment":"/file/2013/06/04/t%C3%A5st%C3%B6%C3%B3%C3%B8%C3%A4.txt",
            "created":"2013-06-04T08:07:24.920"
          }
        ],
        "body":"<p>unicode</p>\n",
        "raw_body":"unicode",
        "collapsed":false,
        "date_created":"2013-06-04T08:07:26.678",
        "date_edited":null,
        "date_latest_activity":"2013-08-28T14:42:54.087",
        "discussion":"/api/v2/discussion/592/",
        "parent":null,
        "root":null,
        "permalink":"/test-sandbox/592/unicode/",
        "read":true,
        "user":"/api/v2/user/c17ec52b925fc0fe2f4eadf1f10b0bf7/",
        "votes":[]
      }
    },
    {
      "id":593,
      "url":"/api/v2/discussion/593/",
      "reply_count":0,
      "intro":"Can someone please mention me?",
      "slug":"mention-me-please",
      "title":"Mention me, please?",
      "unread_count":0,
      "team":"/api/v2/team/9/",
      "message":{
        "id":3790,
        "url":"/api/v2/message/3790/",
        "attachments":[],
        "body":"<p>Can someone please mention me?</p>\n",
        "raw_body":"Can someone please mention me?",
        "collapsed":false,
        "date_created":"2013-07-03T16:09:34.351",
        "date_edited":null,
        "date_latest_activity":"2013-07-03T16:34:25.065",
        "discussion":"/api/v2/discussion/593/",
        "parent":null,
        "root":null,
        "permalink":"/test-sandbox/593/mention-me-please/",
        "read":true,
        "user":"/api/v2/user/f31abb30271cdecae75a6227128c8fd9/",
        "votes":[]
      }
    },
    {
      "id":590,
      "url":"/api/v2/discussion/590/",
      "reply_count":0,
      "intro":"Hey [@Yann Malet](user:/api/v2/user/e7c323f0011c91b78a8bd5e2d6df8d03/) how are yo?",
      "slug":"unicode-mentions",
      "title":"Unicode Mentions",
      "unread_count":0,
      "team":"/api/v2/team/9/",
      "message":{
        "id":3779,
        "url":"/api/v2/message/3779/",
        "attachments":[],
        "body":"<p>Hey <span data-user=\"e7c323f0011c91b78a8bd5e2d6df8d03\" class=\"user-mention\">Yann Malet</span> how are yo?</p>\n",
        "raw_body":"Hey [@Yann Malet](user:/api/v2/user/e7c323f0011c91b78a8bd5e2d6df8d03/) how are yo?",
        "collapsed":false,
        "date_created":"2013-05-09T12:08:54.761",
        "date_edited":null,
        "date_latest_activity":"2013-05-09T13:08:49.379",
        "discussion":"/api/v2/discussion/590/",
        "parent":null,
        "root":null,
        "permalink":"/test-sandbox/590/unicode-mentions/",
        "read":true,
        "user":"/api/v2/user/66238278d2ce670dcb448f96258dc732/",
        "votes":[]
      }
    }
  ]
};


================================================
FILE: test/fixtures/teams.js
================================================
module.exports = [
  {
    "url":"/api/v2/team/9/",
    "name":"Test Sandbox",
    "slug":"test-sandbox",
    "description":"",
    "unread":0,
    "date_created":"2012-01-04T21:51:18.514",
    "members":[
      {
        "is_owner":false,
        "url":"/api/v2/team/9/member/66238278d2ce670dcb448f96258dc732/",
        "date_updated":"2012-01-09T00:00:00",
        "is_admin":false,
        "user":"/api/v2/user/66238278d2ce670dcb448f96258dc732/",
        "date_created":"2012-01-09T00:00:00",
        "notify":2
      },
      {
        "is_owner":false,
        "url":"/api/v2/team/9/member/c17ec52b925fc0fe2f4eadf1f10b0bf7/",
        "date_updated":"2012-01-09T00:00:00",
        "is_admin":false,
        "user":"/api/v2/user/c17ec52b925fc0fe2f4eadf1f10b0bf7/",
        "date_created":"2012-01-09T00:00:00",
        "notify":2
      },
      {
        "is_owner":true,
        "url":"/api/v2/team/9/member/f31abb30271cdecae75a6227128c8fd9/",
        "date_updated":"2012-01-24T20:43:43.180",
        "is_admin":true,
        "user":"/api/v2/user/f31abb30271cdecae75a6227128c8fd9/",
        "date_created":"2012-01-09T00:00:00",
        "notify":2
      },
    ],
    "channel_token":"dabe371eb42f42eb9b82ff471fd73d1b",
    "organization":"Lincoln Loop",
    "post_email":"test-sandbox@example.com"
  }
];


================================================
FILE: test/fixtures/users.js
================================================
module.exports = [
  {
    "url":"/api/v2/user/66238278d2ce670dcb448f96258dc732/",
    "name":"Martin",
    "email":"martin@mahner.org"
  },
  {
    "url": "/api/v2/user/c17ec52b925fc0fe2f4eadf1f10b0bf7/",
    "name": "Michael",
    "email": "michael@lincolnloop.com"
  },
  {
    "url": "/api/v2/user/f31abb30271cdecae75a6227128c8fd9/",
    "name": "Brandon Konkle",
    "email": "brandon@lincolnloop.com"
  }
];
Download .txt
gitextract_t3uznu7z/

├── .editorconfig
├── .gitignore
├── .jshintrc
├── LICENSE
├── README.md
├── amygdala.js
├── bower.json
├── gulpfile.js
├── package.json
├── templates/
│   └── umd.jst
└── test/
    ├── amygdala.test.js
    └── fixtures/
        ├── discussions.js
        ├── teams.js
        └── users.js
Condensed preview — 14 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (59K chars).
[
  {
    "path": ".editorconfig",
    "chars": 173,
    "preview": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_tr"
  },
  {
    "path": ".gitignore",
    "chars": 143,
    "preview": "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*.su"
  },
  {
    "path": ".jshintrc",
    "chars": 470,
    "preview": "{\n  \"bitwise\": true,\n  \"curly\": true,\n  \"eqeqeq\": true,\n  \"esnext\": true,\n  \"expr\": true,\n  \"forin\": false,\n  \"immed\": t"
  },
  {
    "path": "LICENSE",
    "chars": 1498,
    "preview": "Copyright (c) 2014, Lincoln Loop\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or withou"
  },
  {
    "path": "README.md",
    "chars": 6986,
    "preview": "![Amygdala logo](https://raw.githubusercontent.com/lincolnloop/amygdala/master/static/logo.png)\n\nAmygdala is a RESTful H"
  },
  {
    "path": "amygdala.js",
    "chars": 17394,
    "preview": "'use strict';\n\n// CommonJS check so we can require dependencies\nif (typeof module === 'object' && module.exports) {\n  va"
  },
  {
    "path": "bower.json",
    "chars": 595,
    "preview": "{\n  \"name\": \"Amygdala\",\n  \"main\": \"amygdala.js\",\n  \"version\": \"0.4.5\",\n  \"homepage\": \"https://github.com/lincolnloop/amy"
  },
  {
    "path": "gulpfile.js",
    "chars": 1665,
    "preview": "'use strict';\n\nvar _ = require('lodash');\nvar browserify = require('gulp-browserify');\nvar gulp = require('gulp');\nvar p"
  },
  {
    "path": "package.json",
    "chars": 1570,
    "preview": "{\n  \"name\": \"amygdala\",\n  \"version\": \"0.5.0\",\n  \"description\": \"RESTful HTTP library for JavaScript powered web applicat"
  },
  {
    "path": "templates/umd.jst",
    "chars": 1375,
    "preview": "/*\n * Amygdala v<%= pkg.version %>\n * (c) <%= new Date().getFullYear() %> <%= pkg.author %>\n * <%= pkg.homepage %>\n * Li"
  },
  {
    "path": "test/amygdala.test.js",
    "chars": 18005,
    "preview": "/* global describe, it, before, after, beforeEach */\n'use strict';\n\nvar _ = require('lodash');\nvar chai = require('chai'"
  },
  {
    "path": "test/fixtures/discussions.js",
    "chars": 4405,
    "preview": "module.exports = {\n  \"next\":2,\n  \"results\":[\n    {\n      \"id\":595,\n      \"url\":\"/api/v2/discussion/595/\",\n      \"reply_c"
  },
  {
    "path": "test/fixtures/teams.js",
    "chars": 1311,
    "preview": "module.exports = [\n  {\n    \"url\":\"/api/v2/team/9/\",\n    \"name\":\"Test Sandbox\",\n    \"slug\":\"test-sandbox\",\n    \"descripti"
  },
  {
    "path": "test/fixtures/users.js",
    "chars": 414,
    "preview": "module.exports = [\n  {\n    \"url\":\"/api/v2/user/66238278d2ce670dcb448f96258dc732/\",\n    \"name\":\"Martin\",\n    \"email\":\"mar"
  }
]

About this extraction

This page contains the full source code of the lincolnloop/amygdala GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 14 files (54.7 KB), approximately 14.9k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!