master 9e876387f964 cached
18 files
106.9 KB
21.9k tokens
3 symbols
1 requests
Download .txt
Repository: Swimlane/angular-model-factory
Branch: master
Commit: 9e876387f964
Files: 18
Total size: 106.9 KB

Directory structure:
gitextract_esd4rmsq/

├── .gitignore
├── .npmignore
├── .travis.yml
├── CONTRIBUTING.md
├── Gruntfile.js
├── LICENSE
├── README.md
├── bower.json
├── examples/
│   ├── plain/
│   │   └── index.html
│   └── simple.js
├── package.json
├── src/
│   └── modelFactory.js
└── test/
    ├── .jshintrc
    ├── karma.conf-ci.js
    ├── karma.conf.js
    └── spec/
        ├── modelFactory.spec.js
        ├── modelUsage.spec.js
        └── regression.spec.js

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

================================================
FILE: .gitignore
================================================
node_modules/
bower_components/
jspm_packages/
.idea/*
_site/
typings/
dist/
*.log


================================================
FILE: .npmignore
================================================
node_modules/
bower_components/
test/
src/
examples/

================================================
FILE: .travis.yml
================================================
sudo: false
language: node_js
cache:
  directories:
    - node_modules
branches:
  only:
    - master
notifications:
  email: false
node_js:
  - '4.1'
before_install:
  - npm i -g npm@^2.0.0
  - npm install -g grunt
  - npm install -g bower
  - npm install
  - bower install
before_script:
  - npm prune
after_success:
  - npm run semantic-release


================================================
FILE: CONTRIBUTING.md
================================================
Contributing
============

We'd love to get contributions from your part...in the end that's the value behind sharing, right? :smile:
However, for staying organized we'd like you to follow these simple guidelines:

- [Issues](#issues)
- [Commit Message Guidelines](#commit)
- [Coding](#coding)

## <a name="issues"></a> Issues

If you have a bug or enhancement request, please file an issue.

When submitting an issue, please include context from your test and
your application. If there's an error, please include the error text.

The best would be to submit a PR with a failing test :smiley:.

## <a name="commit"></a> Commit Message Guidelines

These guidelines have been taken and adapted from the [official Angular guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines). By following the rules also mentioned in [conventional-changelog](https://www.npmjs.com/package/conventional-changelog). This leads to much more readable and clearer commit messages.

### Commit Message Format
Each commit message consists of a **header**, a **body** and a **footer**.  The header has a special
format that includes a **type**, a **scope** and a **subject**:

```
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
```

The **header** is mandatory and the **scope** of the header is optional.

Any line of the commit message cannot be longer than 100 characters! This allows the message to be easier
to read on GitHub as well as in various git tools.

### Revert
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. In the body it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted.

### Type
Must be one of the following:

* **feat**: A new feature
* **fix**: A bug fix
* **docs**: Documentation only changes
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
  semi-colons, etc)
* **refactor**: A code change that neither fixes a bug nor adds a feature
* **perf**: A code change that improves performance
* **test**: Adding missing tests
* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
  generation

### Scope
The scope could be anything specifying place of the commit change. For example
`olHelper`, `layer`, etc.

### Subject
The subject contains succinct description of the change:

* use the imperative, present tense: "change" not "changed" nor "changes"
* don't capitalize first letter
* no dot (.) at the end

### Body
Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
The body should include the motivation for the change and contrast this with previous behavior.

### Footer
The footer should contain any information about **Breaking Changes** and is also the place to
reference GitHub issues that this commit **Closes**.

**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.

A detailed explanation can be found in this [document][https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit].

## <a name="coding"></a> Coding

Get a fresh copy of this repo.

### Prepare your environment
* Install [Node.js](http://nodejs.org/) and NPM (should come with)
* Install global dev dependencies: `npm install -g bower grunt-cli`
* Install local dev dependencies: `npm install && bower install` in repository directory

### Development Commands
* `grunt build` to concat and build
* `grunt karma` for continuous testing mode with karma (useful during development as tests will be run on each change)
* `grunt karma:ci` for a one-time execution of the tests (used by Travis)


================================================
FILE: Gruntfile.js
================================================
module.exports = function(grunt) {

    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-karma');

    var ngAnnotate = require("ng-annotate");

    grunt.initConfig({
        pkg: grunt.file.readJSON('bower.json'),
        npmpkg: grunt.file.readJSON('package.json'),

        meta: {
          banner: '/**\n' +
          ' * <%= pkg.description %>\n' +
          ' * @version v<%= npmpkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' +
          ' * @link <%= pkg.homepage %>\n' +
          ' * @author <%= pkg.authors.join(", ") %>\n' +
          ' * @license MIT License, http://www.opensource.org/licenses/MIT\n' +
          ' */\n'
        },
        bower: {
          install: {}
        },
        concat: {
          options: {
            //banner: '<%= meta.banner %>\n(function(angular, undefined) {\n\'use strict\';\n',
            banner: '<%= meta.banner %>\n',
            footer: '',
            process: function(src, filepath) {
              var res = ngAnnotate(src, {
                  add: true,
              });

              if (res.errors) {
                  // do something with this, res.errors is now an array of strings
                  throw new Error(res.errors.join("\n"));
              } else {
                  return res.src;
              }
            }
          },
          dist: {
            files: {
              'dist/<%= pkg.name %>.js': [
                'src/**/*.js'
              ],
              'dist/<%= pkg.name %>-bundle.js': [
                'bower_components/deep-diff/releases/deep-diff-0.2.0.min.js',
                'bower_components/uri-templates/uri-templates.js',
                'src/**/*.js'
              ]
            }
          }
        },
        uglify: {
          options: {
            mangle: true,
            banner: '<%= meta.banner %>'
          },
          dist: {
            files: {
              'dist/<%= pkg.name %>.min.js': 'dist/<%= pkg.name %>.js',
              'dist/<%= pkg.name %>-bundle.min.js': 'dist/<%= pkg.name %>-bundle.js'
            }
          }
        },
        karma: {
          dev: {
            configFile: 'test/karma.conf.js',
            singleRun: false,
            autoWatch: true,
            browsers: ['Chrome'],
            reporters: ['mocha']
          },
          ie: {
            configFile: 'test/karma.conf-ci.js',
            singleRun: true,
            autoWatch: false,
            browsers: ['IE'],
            reporters: ['mocha']
          },
          ci: {
            configFile: 'test/karma.conf-ci.js',
            singleRun: true,
            autoWatch: false,
            browsers: ['PhantomJS'],
            reporters: ['mocha']
          }
        }
    });

    grunt.registerTask('build', ['concat', 'uglify']);

    return grunt;
};

================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2014 

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



================================================
FILE: README.md
================================================
# modelFactory 

[![Build Status](https://travis-ci.org/swimlane/angular-model-factory.svg?branch=master)](https://travis-ci.org/Swimlane/model-factory) [![npm version](https://badge.fury.io/js/angular-model-factory.svg)](http://badge.fury.io/js/angular-model-factory) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![Bower version](https://badge.fury.io/bo/angular-model-factory.svg)](http://badge.fury.io/bo/angular-model-factory) [![Codacy Badge](https://www.codacy.com/project/badge/d6659f50bd234f099738358a2a17bf9c)](https://www.codacy.com/public/amcdaniel2/model-factory) [![Join the chat at https://gitter.im/Swimlane/angular-model-factory](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Swimlane/angular-model-factory?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Dependency Status](https://david-dm.org/swimlane/angular-model-factory.svg)](https://david-dm.org/Swimlane/angular-model-factory) [![devDependency Status](https://david-dm.org/swimlane/angular-model-factory/dev-status.svg)](https://david-dm.org/Swimlane/angular-model-factory#info=devDependencies)

A light-weight model layer that bridges the gap between AngularJS and your RESTful APIs.

Why would you use this over other available solutions?

- Lightweight/Simple, the code simply does some basic copy/extending and prototypical instances; no magic required.
- Patterns/Practices, the model definition closely resembles Angular's ngResource meaning its easy to swap out, replace later, eases scaling/transition, and its designed for Angular; not a backbone port!
- Utilizes Angular at the core, it doesn't duplicate things Angular already does.  Any action can be passed a `$http` configuration option, all your interceptors still work, it uses Angular's cache, etc!
- Compliant, URI Template matches the specs.
- Small - 1.45KB gzipped/minified ( excludes depedencies )
- Minimal Dependencies, only use URI template and deep-diff ( this isn't even required ) utility.  NO underscore, lodash, jquery, etc!
- Its full of awesome features


See [wiki](https://github.com/swimlane/model-factory/wiki) for documentation.


## Features

- URI Templates (RFC6570)
- Model instances
- Collections
- Single Datastore
- Caching / Cache invalidation
- Default value population
- Pending / Completed Status
- Relationships
- Object Deep Diff / Reversion
- Track active promises to prevent duplicate sends


## Other Solutions

After doing quite a bit of research before writing this, I took a look at other solutions.  Here is what I found and why I wrote my own.

- [Restmod](https://github.com/platanus/angular-restmod)
Very nice solution but very opinionated and hyper-active. 22kb min

- [Modelizer](https://github.com/VasilioRuzanni/angular-modelizer)
Good but requires Lodash. 23kb min

- [ModelCore](https://github.com/klederson/ModelCore/)
Good but not very well tested and not active.

- [angular-watch-resource](https://github.com/marmorkuchen-net/angular-watch-resource) - Really only handles collections

- [angular-restful](http://esdrasedu.github.io/angular-restful/#/) - Very basic but nice

- [ngResource](https://docs.angularjs.org/api/ngResource/service/$resource)
Out of the box model layer, very limited.

- [angularjs-rails-resource](https://github.com/FineLinePrototyping/angularjs-rails-resource)
Too rails-ish.

- [angular-nested-resource](https://github.com/roypeled/angular-nested-resource) - Okay API, not loving the nested architecture.

- [Aar.js](http://aarjs.com/)
Very light, not sure what value this adds.

- [Angular Activerecord](https://github.com/bfanger/angular-activerecord)
A copy of BackboneModel but doesn't really work with Angular patterns.

- [Angular-Data](http://angular-data.pseudobry.com/)
Not really a model layer but a data store.  Very very heavy ( 67kb min )

- [ngActiveResource](https://github.com/FacultyCreative/ngActiveResource)
Very ruby-ish api.  Requires lodash.  Has validation but thats not needed in angular if you do it right.

- [restangular](https://github.com/mgonto/restangular) 
I don't consider this a model layer; it feels moore like a fancy http layer that returns promises because everyone complains about ngResource not doing it.  It requires underscore.

- [BreezeJS](http://www.breezejs.com/) 
This is a very full featured model/cache/validation etc.  Its framework agnostic, which means it follows its own patterns and not angulars.  Its very heavy, requires server data massaging, and the API looks like Microsoft Entity Framework.

- [ng-backbone](https://github.com/adrianlee44/ng-backbone)
Another backbone model clone.  This one actually requires backbone and lodash.

## Install

Install via bower:

```
$ bower install angular-model-factory --save
```

Install via npm:

```
$ npm install angular-model-factory --save
```

Alternatively you can download/clone the repo and link the files in `dist/`. 

### Dependencies

- Angular >= 1.3
- [deep-diff](https://github.com/flitbit/diff)
- [uri-templates](https://github.com/geraintluff/uri-templates)


## Contribute

Libraries like this live and get better with an active community. Have something to contribute? We'd love to see it. Just head over to our [contribution guidelines](CONTRIBUTING.md).

## Credits

`angular-model-factory` is a [Swimlane](http://swimlane.com) open-source project; we believe in giving back to the open-source community by sharing some of the projects we build for our application. Swimlane is an automated cyber security operations and incident response platform that enables cyber security teams to leverage threat intelligence, speed up incident response and automate security operations.


================================================
FILE: bower.json
================================================
{
  "name": "angular-model-factory",
  "authors": [
    "Austin McDaniel <amcdaniel2@gmail.com>",
    "Juri Strumpflohner <juri.strumpflohner@gmail.com>"
  ],
  "description": "modelFactory makes working with RESTful APIs in AngularJS easy",
  "license": "MIT",
  "homepage": "http://swimlane.github.io/angular-model-factory/",
  "repository": {
    "type": "git",
    "url": "git://github.com/swimlane/model-factory.git"
  },
  "main": "./dist/angular-model-factory.js",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "src",
    "examples"
  ],
  "dependencies": {
    "uri-templates": "~0.1.5",
    "deep-diff": "~0.2.0",
    "angular": "1.3.x"
  },
  "devDependencies": {
    "angular-mocks": "1.x",
    "angular-scenario": "1.x"
  },
  "resolutions": {
    "angular": "1.4.9"
  }
}


================================================
FILE: examples/plain/index.html
================================================
<!doctype html>
<html>
<head>
</head>
<body ng-app="demo">
    <h1>Model Factory demo</h1>




    <script type="text/javascript" src="../../bower_components/angular/angular.js"></script>
    <script type="text/javascript" src="../../bower_components/uri-templates/uri-templates.min.js"></script>
    <script type="text/javascript" src="../../bower_components/deep-diff/releases/deep-diff-0.2.0.min.js"></script>
    <script type="text/javascript" src="../../dist/angular-model-factory.js"></script>

    <script type="text/javascript">
        var app = angular.module('demo', ['modelFactory'])
            .factory('PersonModel', function($modelFactory){
                return $modelFactory('/api/people');
            })
            .controller('DemoCtrl', function(PersonModel){
                var vm = this;
            });
    </script>
</body>
</html>

================================================
FILE: examples/simple.js
================================================
define(['angular', 'model-factory'], function (angular) {

    var module = angular.module('myapp', ['modelFactory']);
    
    module.factory('AnimalModel', function() {
        function Animal(val) {
            angular.extend(this, val);
        };

        Animal.prototype.dateAdded = function(val) {
            return new Date(val);
        };

        return Animal;
    });

    module.factory('ZooModel', function($modelFactory, AnimalModel){
        return $modelFactory('api/zoo', {
            defaults: {
                zooName: 'New Zoo'
            },
            map: {
                animals: function(animal){
                    return animal.map(function(a){ return new AnimalModel(a); })
                }    
            },
            actions: {
                query: {
                    cache: true
                },
                $copy: {
                    method: 'POST',
                    url: 'copy'
                }
            }
        });
    });

    module.config(function ($stateProvider) {
        $stateProvider.state('zoo', {
            url: '/zoo',
            templateUrl: 'zoo.tpl.html',
            controller: 'ZooCtrl',
            resolve: {
                zoos: function (ZooModel) {
                    return ZooModel.query();
                }
            }
        });
    });

    module.controller('ZooCtrl', function ($scope, ZooModel, zoos) {

        //-> zoos = [ ZooModel({ type: 'National', name: 'DC Zoo', id: '123' }) ]
        $scope.zoos = zoos;

        $scope.editZoo = function(zoo){
            // UPDATE since we have an id
            zoo.$save().then(function(model){

                // cache was invalidated
                alert('Zoo was updated');
            });
        };

        $scope.deleteZoo = function(zoo){
            zoo.$destroy().then(function(){

                // cache was invalidated
                alert('Zoo was deleted');

                // zoo was automatically removed from array
                //-> $scope.zoos = [];
            });
        };

        $scope.createZoo = function(name){
            var newZoo = new ZooModel({
                name: name
            });

            // POST since no id
            zooModel.$save().then(function(model){
                //-> ZooModel({ name: 'whatever', newAnimal: true })

                // push our new zoo into the model
                // this will relate the zoo to the model
                // so if a user destorys this model it will
                // automatically be removed from the array too
                $scope.zoos.push(model);
            });
        };

        $scope.getZoo = function(id){
            // get a zoo by id
            ZooModel.get(id).then(function(model){
                $scope.newZoo = model;
            });
        };

        $scope.getPandas = function(){
            // get a zoo by animalType of panda
            // GET api/zoo?animalType=panda
            ZooModel.query({ animalType: 'panda' }).then(function(models){
                $scope.pandas = models;
            });
        };

        $scope.refreshZoos = function(){
            ZooModel.query().then(function(models){
                // models = [ ZooModel({ type: 'National', name: 'DC Zoo', id: '123' }) ]
                $scope.allZoos = models;
            });
        };

    });

    return module;
});


================================================
FILE: package.json
================================================
{
  "name": "angular-model-factory",
  "author": "Austin McDaniel <amcdaniel2@gmail.com>",
  "authors": [
    "Juri Strumpflohner <juri.strumpflohner@gmail.com>"
  ],
  "description": "modelFactory makes working with RESTful APIs in AngularJS easy",
  "license": "MIT",
  "devDependencies": {
    "grunt": "~0.4.5",
    "grunt-bower": "*",
    "grunt-cli": ">= 0.1.7",
    "grunt-contrib-concat": "*",
    "grunt-contrib-jshint": "*",
    "grunt-contrib-uglify": "*",
    "grunt-karma": "~0.9.0",
    "jasmine-core": "~2.1.2",
    "karma": "~0.12.28",
    "karma-chrome-launcher": "~0.1.5",
    "karma-firefox-launcher": "~0.1.3",
    "karma-ie-launcher": "^0.2.0",
    "karma-jasmine": "~0.3.1",
    "karma-mocha-reporter": "~0.3.1",
    "karma-phantomjs-launcher": "~0.1.4",
    "ng-annotate": "^1.0.2",
    "publish-latest": "^1.1.2",
    "semantic-release": "^4.3.5"
  },
  "main": "./dist/angular-model-factory.js",
  "scripts": {
    "build": "grunt build",
    "prepublish": "npm run build",
    "postpublish": "publish-latest",
    "test": "grunt karma:ci",
    "semantic-release": "semantic-release pre && npm publish && semantic-release post"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/Swimlane/angular-model-factory.git"
  }
}


================================================
FILE: src/modelFactory.js
================================================
/* global angular:false */
'use strict';

(function(global, factory) {
    if (typeof define === 'function' && define.amd) {
        define(['angular', 'uri-templates', 'deep-diff'], factory);
    } else if (typeof module !== 'undefined' && module.exports) {
        module.exports = factory(require('angular'), require('uri-templates'), require('deep-diff'));
    } else {
        global.ModelFactory = factory(global.angular, global.UriTemplate, global.DeepDiff);
    }
})(this, function(angular, UriTemplate, DeepDiff) {

var module = angular.module('modelFactory', []);

// compression
var isUndefined = angular.isUndefined,
    forEach = angular.forEach,
    extend = angular.extend,
    copy = angular.copy;

// keywords that are reserved for model instance
// internal usage only and to be stripped
// before sending to server
var instanceKeywords = [ '$$array', '$save', '$destroy',
    '$pending', '$rollback', '$diff', '$update', '$commit', '$copy' ];

// keywords that are reserved for the model static
// these are used to determine if a attribute should be extended
// to the model static class for like a helper that is not a http method
var staticKeywords = [ 'actions', 'instance', 'list', 'defaults',
    'pk', 'stripTrailingSlashes', 'map'];

// Deep extends
// http://stackoverflow.com/questions/15310935/angularjs-extend-recursive
var extendDeep = function (dst) {
    forEach(arguments, function (obj) {
        if (obj !== dst) {
            forEach(obj, function (value, key) {
                if (instanceKeywords.indexOf(key) === -1) {
                    if (dst[key]) {
                        if (angular.isArray(dst[key])) {
                            dst[key].concat(value.filter(function (v) {
                                var vv = dst[key].indexOf(v) !== -1;
                                if (vv) extendDeep(vv, v);
                                return vv;
                            }));
                        } else if (angular.isObject(dst[key])) {
                            extendDeep(dst[key], value);
                        } else {
                            // if value is a simple type like a string, boolean or number
                            // then assign it
                            dst[key] = value;
                        }
                    } else if (!angular.isFunction(dst[key])) {
                        dst[key] = value;
                    }
                }
            });
        }
    });
    return dst;
};

// Create a shallow copy of an object and clear other fields from the destination
// https://github.com/angular/angular.js/blob/master/src/ngResource/resource.js#L30
var shallowClearAndCopy = function(src, dst) {
    dst = dst || {};

    // Remove any properties in destination that were not
    // returned from the source
    forEach(dst, function (value, key) {
        if(!src.hasOwnProperty(key) && key.charAt(0) !== '$') {
            delete dst[key];
        }
    });

    for(var key in src) {

        if(src.hasOwnProperty(key) && key.charAt(0) !== '$')  {
            // For properties common to both source and destination,
            // check for object references and recurse as needed. Route around
            // arrays to prevent value/order inconsistencies
            if(angular.isObject(src[key]) && !angular.isArray(src[key])) {
                dst[key] = shallowClearAndCopy(src[key], dst[key]);
            } else {
                // Not an object, so just overwrite with value from source
                dst[key] = src[key];
            }
        }
    }

    return dst;
};


module.provider('$modelFactory', function(){
    var provider = this;

    provider.defaultOptions = {

        /**
         * URL Prefix for requests.  This should only really
         * be used at the provider level, not an instance.
         */
        prefix: '',

        /**
         * Primary key of the model
         */
        pk: 'id',

        /**
         * By default, trailing slashes will be stripped
         * from the calculated URLs.
         */
        stripTrailingSlashes: true,

        /**
         * Default values for a new instance.
         * This will only be populated if the property
         * is undefined.
         *
         * Example:
         *      defaults: {
         *          'create': new Date()
         *      }
         */
        defaults: {},

        /**
         * Attribute mapping.  Tranposes attributes
         * from a response to a different attribute.
         *
         * Also handles 'has many' and 'has one' relations.
         *
         * Example:
         *      map: {
         *          // transpose `animalId` to
         *          // `id` on our instance
         *          'id': 'animalId',
         *
         *          // transposes `animal` attribute
         *          // to an array of `AnimalModel`'s
         *          'animal': AnimalModel.List,
         *
         *          // transposes `location` attribute
         *          // to an instance of `LocationModel`
         *          'location': LocationModel
         *      }
         */
        map:{},

        /**
         * Hash declaration of model actions.
         *
         * NOTE: Anything prefixed with `$` will be attached to the
         * model instance rather than the static.
         */
        actions:{

            /**
             * Base options to be applied to all other actions by default.
             * In addition to the methods listed here, any `$http` attribute
             * is valid. https://docs.angularjs.org/api/ng/service/$http
             *
             * If the method is a `GET` and the arguments invoking it are a string or number,
             * then the model automatically assumes you are wanting to pass those are the primary key.
             *
             * Action Agnostic Attributes:
             *  - `override` Overrides the base url prefixing.
             *  - `method` Case insensitive HTTP method (e.g. GET, POST, PUT, DELETE, JSONP, etc).
             *  - `url` URL to be invoked by `$http`.  All urls are prefixed with the base url passed initally.  All templates are [URI Template](http://tools.ietf.org/html/rfc6570) spec.
             */
            'base': {
                /**
                 * Wrap the response from an action in a instance of the model.
                 */
                wrap: true,

                /**
                 * Callback before data is sent to server.
                 * This allows developers to manipulate the
                 * object before its sent to the server but
                 * not effect the core object.
                 */
                beforeRequest: undefined,

                /**
                 * Callback after data recieved from server but
                 * before the data is wrapped in an instance.
                 */
                afterRequest: undefined,

                /**
                 * By default, do not cache the requests.
                 */
                cache: false
            },
            'get': {
                method: 'GET'
            },
            'query': {
                method: 'GET',

                /**
                 * If true then the returned object for this action is an array.
                 */
                isArray: true
            },

            /**
             * In theory `post`, `update`, and `delete` below would/should not be used,
             * instead one would use `$save` or `$destroy` to be invoked
             */
            'post': {
                method: 'POST',
                invalidateCache: true
            },
            'update': {
                method: 'PUT',
                invalidateCache: true
            },
            'delete': {
                method: 'DELETE',
                invalidateCache: true
            }
        },

        /**
         * Instance level extensions/helpers.
         *
         * Example:
         *      instance: {
         *          'name': function() {
         *              return this.first + ' ' + this.last
         *          }
         *      }
         */
        instance: {},

        /**
         * List level extensions/helpers.
         *
         * Example:
         *
         *      list: {
         *          'namesById': function(id){
         *              return this.find(function(u){ return u.id === id; });
         *          }
         *      }
         *
         */
        list: {}
    };

    provider.$get = ['$rootScope', '$http', '$q', '$log', '$cacheFactory', function($rootScope, $http, $q, $log, $cacheFactory) {

        /**
         * Model factory.
         *
         * Example usages:
         *       $modelFactory('api/zoo');
         *       $modelFactory('api/zoo', { ... });
         */
        function modelFactory(url, options) {

            /**
             * Prevents multiple calls of the exact same type.
             *
             *      { key: url, value: promise }
             *
             */
            var promiseTracker = {};

            /**
             * Make a pretty name from the url
             * for the event emitters
             */
            var nameSplit = url.split('/'),
                prettyName = nameSplit[nameSplit.length - 1];

            // copy so we also extend our defaults and not override
            //var actions = angular.extend({}, defaultOptions.actions, options.actions);
            options = extendDeep({}, copy(provider.defaultOptions), options);

            //
            // Collection
            // ------------------------------------------------------------
            //

            /**
             * Model list instance.
             * All raw objects passed will be converted to an instance of this model.
             *
             * If we `push` a item into an existing collection, a pointer will be made
             * so on a destroy items will be removed from the array as well.
             *
             * Example usages:
             *       var zoos = new Zoo.List([ {}, ... ]);
             */
            function ModelCollection(value){
                value = value || [];

                // wrap each obj
                value.forEach(function(v, i){
                    // this should not happen but prevent blow up
                    if(v === null || v === undefined) return;

                    // reset to new instance
                    value[i] = wrapAsNewModelInstance(v, value);
                });

                // override push to set an instance
                // of the list on the model so destroys will chain
                var __oldPush = value.push;
                value.push = function(){
                    // Array.push(..) allows to pass in multiple params
                    var args = Array.prototype.slice.call(arguments);

                    for(var i=0; i<args.length; i++){
                        args[i] = wrapAsNewModelInstance(args[i], value);
                    }

                    __oldPush.apply(value, args);
                };

                // add list helpers
                if(options.list){
                    extend(value, options.list);
                }

                return value;
            };

            // helper function for creating a new instance of a model from
            // a raw JavaScript obj. If it is already a model, it will be left
            // as it is
            var  wrapAsNewModelInstance = function(rawObj, arrayInst){
                // create an instance
                var inst = rawObj.constructor === Model ?
                    rawObj : new Model(rawObj);

                // set a pointer to the array
                inst.$$array = arrayInst;

                return inst;
            };

            // ES5, IE compatible version to retrieve the name of a function. ES6
            // would permit to do something like functionRef.name
            var functionName = function(fun){
                var ret = fun.toString();
                ret = ret.substr('function '.length);
                ret = ret.substr(0, ret.indexOf('('));
                return ret;
            };

            //
            // Model Instance
            // ------------------------------------------------------------

            /**
             * Model instance.
             *
             * Example usages:
             *       var zoo = new Zoo({ ... });
             */
            function Model(value) {
                var instance = this,
                    commits = [];

                // if the value is undefined, create a empty obj
                value = value || {};

                // build the defaults but only on new instances
                forEach(options.defaults, function(v, k){
                    // only populates when not already defined
                    if(value[k] === undefined){
                        if(typeof v === 'function'){
                            // pass the value so you can combine things
                            // this could be tricky if you have defaults that rely on other defaults ...
                            // like: { name: function(val) { return val.firstName + val.lastName }) }
                            value[k] = copy(v(value));
                        } else {
                            value[k] = copy(v);
                        }
                    }
                });

                // Map all the objects to new names or relationships
                forEach(options.map, function(v, k){
                    if (functionName(v) === functionName(Model) || functionName(v) === functionName(ModelCollection)) {
                        value[k] = new v(value[k]); // jshint ignore:line
                    } else if (typeof v === 'function') {
                        // if its a function, invoke it,
                        // this would be helpful for seralizers
                        // like: map: { date: function(val){ return moment(val) } }
                        value[k] = v(value[k], value);
                    } else {
                        value[k] = value[v];
                        delete value[v];
                    }
                });

                // attach instance actions
                forEach(options.actions, function(v, k){
                    if(k[0] === '$'){
                        instance[k] = function(){
                            return Model.$buildRequest(k, v, instance);
                        };
                    }
                });

                // copy values to the instance
                extend(instance, value);

                // copy instance level helpers to this instance
                extend(instance, copy(options.instance));

                /**
                 * Save the instance to the server.  Posts the instance unless
                 * the instance has the `pk` attribute already then it will do a put.
                 */
                instance.$save = function(){
                     var actionType = instance[options.pk] ? 'update' : 'post',
                         promise = Model[actionType](this);

                    instance.$pending = true;

                    promise.then(function(value){
                        instance.$pending = false;

                        // extend the value from the server to me
                        if (value) {
                            instance.$update(value);
                        }

                        var broadcastName = actionType === 'post' ? 'created' : 'updated';
                        $rootScope.$broadcast(prettyName + '-' + broadcastName, instance);

                        // commit the change for reversion
                        commits.push(angular.toJson(instance));
                    }, function () {
                        // rejected
                        instance.$pending = false;
                    });

                    return promise;
                };

                /**
                 * Delete the instance.  Performs a DELETE on this instance performing
                 * the delete action passing an instance of itself.
                 *
                 * If the item is associated with an array, it will automatically be removed
                 * on successful delete.
                 */
                instance.$destroy = function(){
                    // keep a local pointer since we strip before send

                    var promise = Model.delete(this);
                    instance.$pending = true;

                    promise.then(function(){
                        instance.$pending = false;

                        var arr = instance.$$array;
                        if(arr){
                            arr.splice(arr.indexOf(instance), 1);
                        }

                        $rootScope.$broadcast(prettyName + '-destroyed', instance);
                    }, function(){
                        // rejected
                        instance.$pending = false;
                    });

                    return promise;
                };

                /**
                 * Display the difference between the original data and the
                 * current instance.
                 * https://github.com/flitbit/diff
                 */
                instance.$diff = function(version){
                    var prevCommit = commits[version || commits.length - 1],
                        currCommit = angular.toJson(instance);

                    return DeepDiff.diff(JSON.parse(prevCommit), JSON.parse(currCommit), function(path, key) {
                        return key[0] === '$';
                    });
                };


                /**
                 * Commits the change the commits bucket for rollback later if needed.
                 */
                instance.$commit = function () {
                    // stringify it so you have a clean instance
                    commits.push(angular.toJson(instance));
                    return instance;
                };

                /**
                 * Reverts the current instance back either the latest instance
                 * or you can pass a specific instance on the commits stack.
                 */
                instance.$rollback = function(version) {
                    var prevCommit = commits[version || commits.length - 1];
                    instance.$update(JSON.parse(prevCommit));
                    return instance;
                };

                /**
                 * Extends the properties of the new object onto
                 * the current object without replacing it.  Helpful
                 * when copying and then re-copying new props back
                 */
                instance.$update = function(n){
                    shallowClearAndCopy(n, instance);
                    return instance;
                };


                /**
                 * Creates a copy by taking the raw data values and by
                 * creating a new instance of the model.
                 */
                instance.$copy = function(){
                  // get the raw data of the model
                  var rawData = angular.toJson(this);

                  // ..then wrap it into a new instance to create a clone
                  return new Model(angular.fromJson(rawData));
                };

                // Create a copy of the value last so we get all the goodies,
                // like default values and whatnot.
                instance.$commit();
            }

            //
            // Model Static
            // ------------------------------------------------------------

            /**
             * Create an instance of a cache factory
             * for tracking data of this instance type.
             * https://docs.angularjs.org/api/ng/service/$cacheFactory
             */
            Model.$cache = $cacheFactory(url);

            // attach actions
            forEach(options.actions, function(v, k){
                // don't do base or $
                if(k === 'base' || k[0] === '$') return;
                Model[k] = function(){
                    //http://stackoverflow.com/questions/2091138/why-doesnt-join-work-with-function-arguments
                    var args = Array.prototype.slice.call(arguments);
                    return Model.$buildRequest.apply(this, [k, v].concat(args));
                };
            });

            /**
             * Builds the request for a set of actions.
             */
            Model.$buildRequest = function(action, param, data, extras){
                var clone = copy(options.actions.base);
                extend(clone, copy(param));

                // if we explicity call cache
                // to true and don't pass a factory
                // lets use our instance level for
                // data storage means
                if(clone.cache === true){
                    clone.cache = Model.$cache;
                }

                // make sure we have a method specified, otherwise
                // default to GET
                clone.method = clone.method || 'GET';

                // uri template to parameterize
                var uri = options.prefix ? options.prefix + '/' : '';

                // make sure we didn't override the base url prefixing
                if(!clone.override){

                    // set the uri to the base
                    uri += url;

                    // if we have a url defined, append to base
                    if(clone.url) {
                        //check if we need to add slash, or we only duplicate it
                        var requiredSlash = !/^(\/|{\/)/.test(clone.url);
                        uri += requiredSlash ? '/' + clone.url : clone.url;
                    }


                    // set the uri to the base
                    uri = Model.$url(uri, data, clone.method);

                    // attach the pk referece by default if it is a 'core' type
                    if(action === 'get' || action === 'post' || action === 'update' || action === 'delete'){
                        uri += '{/' + options.pk + '}';
                    }

                    if(clone.method === 'GET' && (angular.isString(data) || angular.isNumber(data))){
                        // if we have a get method and its a number or a string
                        // you can assume i'm wanting to do something like:
                        // ZooModel.get(1234) instead of ZooModel.get({ id: 1234 });
                        var obj = {};
                        obj[options.pk] = data;
                        data = obj;

                        // if we have a extra argument on this case we should assume its a
                        //
                        if(extras){
                            // data.param = extras;
                            clone.params = extendDeep({}, clone.params, extras);
                            // uri += '{?param*}';
                        }
                    } else if(clone.method === 'GET' && angular.isObject(data)){
                        // if its a GET request and its not the above, we can assume
                        // you want to do a query param like:
                        // ZooModel.query({ type: 'panda' }) and do /api/zoo?type=panda
                        // data = { param: data };
                        clone.params = extendDeep({}, clone.params, data);
                        // uri += '{?param*}';
                    }
                } else {
                    uri = clone.url;
                }

                clone.url = Model.$url(uri, data, clone.method);

                // don't include the payload for DELETE requests
                if(action !== 'delete' && clone.method !== 'DELETE'){
                    clone.data = data;
                }

                return Model.$call(clone);
            };

            /**
             * Invokes `$http` given parameters and does some
             * callback before/after and state setting.
             */
            Model.$call = function(params){
                // if we have the promise in queue, return it
                var signature = params.method + ':' + params.url;
                if (promiseTracker[signature]) {
                    return promiseTracker[signature];
                }

                var def = $q.defer();

                // set the queue for this promise
                promiseTracker[signature] = def.promise;

                // copy the data so we can manipulate
                // it before the request and not affect
                // the core object
                params.data = copy(params.data);

                // before callbacks
                params.beforeRequest &&
                    params.beforeRequest(params);

                // strip all the internal functions/etc
                params.data = Model.$strip(params.data);

                $http(params).then(function(response){
                    // after callbacks
                    if(params.afterRequest) {
                        var transform = params.afterRequest(response.data);
                        if(!isUndefined(transform)) {
                            response.data = transform;
                        }
                    }

                    // if we had a cache, remove it
                    // this could be optimized to only do
                    // the invalidation of things by id/etc
                    if(params.invalidateCache){
                        Model.$cache.removeAll();
                    }

                    if (response) {
                        if (params.wrap) {
                            if (params.isArray) {
                                def.resolve(new Model.List(response.data));
                            } else {
                                def.resolve(new Model(response.data));
                            }
                        } else {
                            def.resolve(response.data);
                        }
                    } else {
                        def.resolve();
                    }
                }, def.reject).finally(function () {
                    promiseTracker[signature] = undefined;
                });

                return def.promise;
            };

            /**
             * Returns a url given the URI template and parameters.
             *
             * Examples:
             *
             *      // obj = { id: 2344 }
             *      Model.$url('api/zoo/{id}', obj)
             *      //-> 'api/zoo/2345'
             *
             *      // {}
             *      Model.$url('api/zoo/{id}')
             *      //-> 'api/zoo'
             *
             *      // { params: { type: 'panda' } }
             *      Model.$url('api/zoo/{?params*}')
             *      //-> 'api/zoo?type=panda'
             *
             * Optionally strips trailing `/`'s.
             *
             * Based on:
             * https://github.com/geraintluff/uri-templates
             */
            Model.$url = function(u, params, method){
                var uri = new UriTemplate(u || url)
                            .fill(function(variableName){
                                var resolvedVariable = params[variableName];

                                // if we have a match, substitute and remove it
                                // from the original params object
                                if(resolvedVariable){
                                    // only remove params on GET requests as the
                                    // passed object is intended to be used
                                    // as URL params. For persistent HTTP calls
                                    // the object has to be left as it is (for now)
                                    if(method === 'GET'){
                                      delete params[variableName];
                                    }

                                    return resolvedVariable;
                                }else{
                                    // ?? log an error??
                                    return null;
                                }
                            });
                            // .fillFromObject(params || {});

                if(options.stripTrailingSlashes){
                    uri = uri.replace(/\/+$/, '') || '/';
                }

                return uri;
            };

            /**
             * Remove instances of reserved keywords
             * before sending to server/json.
             */
            Model.$strip = function(args){
                // todo: this needs to account for relationships too?
                // either make recursive or chain invoked
                if(args && typeof args === 'object'){
                    forEach(args, function(v,k){
                        if(instanceKeywords.indexOf(k) > -1){
                            delete args[k];
                        }
                    });
                }
                return args;
            };

            // extend the static class with arguments that are not internal
            forEach(options, function(v, k){
                if(staticKeywords.indexOf(k) === -1){
                    Model[k] = v;
                }
            });

            // has to be at end for depedency reasons
            Model.List = ModelCollection;

            return Model;
        }

        return modelFactory;
    }];
});

return module;
});


================================================
FILE: test/.jshintrc
================================================
{
  "node": true,
  "browser": true,
  "esnext": true,
  "bitwise": true,
  "camelcase": true,
  "curly": true,
  "eqeqeq": true,
  "immed": true,
  "indent": 2,
  "latedef": true,
  "newcap": true,
  "noarg": true,
  "quotmark": "single",
  "regexp": true,
  "undef": true,
  "unused": true,
  "strict": true,
  "trailing": true,
  "smarttabs": true,
  "globals": {
    "after": false,
    "afterEach": false,
    "angular": false,
    "before": false,
    "beforeEach": false,
    "browser": false,
    "describe": false,
    "expect": false,
    "inject": false,
    "it": false,
    "jasmine": false,
    "spyOn": false
  }
}



================================================
FILE: test/karma.conf-ci.js
================================================
// Karma configuration
// http://karma-runner.github.io/0.12/config/configuration-file.html
// Generated on 2014-11-27 using
// generator-karma 0.8.2

module.exports = function(config) {
  config.set({
    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,

    // base path, that will be used to resolve files and exclude
    basePath: '../',

    // testing framework to use (jasmine/mocha/qunit/...)
    frameworks: ['jasmine'],

    // list of files / patterns to load in the browser
    files: [
      'bower_components/angular/angular.js',
      'bower_components/angular-mocks/angular-mocks.js',
      // 'bower_components/angular-scenario/angular-scenario.js',
      // 'bower_components/deep-diff/index.js',
      // 'bower_components/uri-templates/uri-templates.js',
      // 'bower_components/angular-animate/angular-animate.js',
      // 'bower_components/angular-cookies/angular-cookies.js',
      // 'bower_components/angular-resource/angular-resource.js',
      // 'bower_components/angular-route/angular-route.js',
      // 'bower_components/angular-sanitize/angular-sanitize.js',
      // 'bower_components/angular-touch/angular-touch.js',
      'dist/angular-model-factory-bundle.min.js',
      'test/mock/**/*.js',
      'test/spec/**/*.js'
    ],

    // list of files / patterns to exclude
    exclude: [],

    // web server port
    port: 9876,

    // Start these browsers, currently available:
    // - Chrome
    // - ChromeCanary
    // - Firefox
    // - Opera
    // - Safari (only Mac)
    // - PhantomJS
    // - IE (only Windows)
    browsers: [
      'PhantomJS'
    ],

    // if the browser doesn't capture within the given ms, kill it
    captureTimeout: 60000,

    // Which plugins to enable
    // plugins: [
    //   'karma-phantomjs-launcher',
    //   'karma-jasmine'
    // ],

    // Continuous Integration mode
    // if true, it capture browsers, run tests and exit
    singleRun: false,

    colors: true,

    // level of logging
    // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
    logLevel: config.LOG_INFO,

    // Uncomment the following lines if you are using grunt's server to run the tests
    // proxies: {
    //   '/': 'http://localhost:9000/'
    // },
    // URL root prevent conflicts with the site root
    // urlRoot: '_karma_'
  });
};

================================================
FILE: test/karma.conf.js
================================================
// Karma configuration
// http://karma-runner.github.io/0.12/config/configuration-file.html
// Generated on 2014-11-27 using
// generator-karma 0.8.2

module.exports = function(config) {
  config.set({
    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,

    // base path, that will be used to resolve files and exclude
    basePath: '../',

    // testing framework to use (jasmine/mocha/qunit/...)
    frameworks: ['jasmine'],

    // list of files / patterns to load in the browser
    files: [
      'bower_components/angular/angular.js',
      'bower_components/angular-mocks/angular-mocks.js',
      // 'bower_components/angular-scenario/angular-scenario.js',
      'bower_components/deep-diff/index.js',
      'bower_components/uri-templates/uri-templates.js',
      // 'bower_components/angular-animate/angular-animate.js',
      // 'bower_components/angular-cookies/angular-cookies.js',
      // 'bower_components/angular-resource/angular-resource.js',
      // 'bower_components/angular-route/angular-route.js',
      // 'bower_components/angular-sanitize/angular-sanitize.js',
      // 'bower_components/angular-touch/angular-touch.js',
      'src/**/*.js',
      'test/mock/**/*.js',
      'test/spec/**/*.js'
    ],

    // list of files / patterns to exclude
    exclude: [],

    // web server port
    port: 9876,

    // Start these browsers, currently available:
    // - Chrome
    // - ChromeCanary
    // - Firefox
    // - Opera
    // - Safari (only Mac)
    // - PhantomJS
    // - IE (only Windows)
    browsers: [
      'PhantomJS'
    ],

    // if the browser doesn't capture within the given ms, kill it
    captureTimeout: 60000,

    // Which plugins to enable
    // plugins: [
    //   'karma-phantomjs-launcher',
    //   'karma-jasmine'
    // ],

    // Continuous Integration mode
    // if true, it capture browsers, run tests and exit
    singleRun: false,

    colors: true,

    // level of logging
    // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
    logLevel: config.LOG_INFO,

    // Uncomment the following lines if you are using grunt's server to run the tests
    // proxies: {
    //   '/': 'http://localhost:9000/'
    // },
    // URL root prevent conflicts with the site root
    // urlRoot: '_karma_'
  });
};


================================================
FILE: test/spec/modelFactory.spec.js
================================================
'use strict';

/*
  Specs that test the inner workings of the model-factory. Regression
  tests can be placed in here.
*/

describe('A person model defined using modelFactory', function() {
    var PersonModel;
    var $httpBackend;

    beforeEach(angular.mock.module('modelFactory'));

    beforeEach(function() {
        angular.module('test-module', ['modelFactory'])
            .factory('PersonModel', function($modelFactory) {
                return $modelFactory('/api/people');
            });
    });

    beforeEach(angular.mock.module('test-module'));

    beforeEach(inject(function(_$httpBackend_, _PersonModel_) {
        $httpBackend = _$httpBackend_;
        PersonModel = _PersonModel_;
    }));

    afterEach(function() {
        $httpBackend.verifyNoOutstandingExpectation();
        $httpBackend.verifyNoOutstandingRequest();
    });

    // describe('when calling model.$strip', function(){
    //     it('should remove all model-factory specific functions', function(){
    //         var raw = {
    //             id: 1,
    //             name: 'Juri'
    //         };

    //         expect(new PersonModel(raw).$strip).toEqual(raw);
    //     });
    // });

    describe('when copying a model object', function() {

        it('calling $save on a new model should submit the copied values', function() {
            var newModel = new PersonModel({
                name: 'Juri'
            });

            var copied = newModel.$copy(); //angular.copy(newModel);
            copied.name = 'Austin';

            $httpBackend.expectPOST('/api/people', JSON.stringify(copied)).respond(200, '');

            copied.$save();
            $httpBackend.flush();
        });

        it('calling $save on it should submit the copied values', function() {
            var newModel = new PersonModel({
                name: 'Juri'
            });

            var copied = newModel.$copy(); //angular.copy(newModel);
            copied.name = 'Austin';

            $httpBackend.expectPOST('/api/people', JSON.stringify(copied)).respond(200, '');

            copied.$save();
            $httpBackend.flush();
        });

        it('calling $destroy on it should submit the copied values', function() {
            var newModel = new PersonModel({
                id: 1,
                name: 'Juri'
            });

            var copied = newModel.$copy(); // angular.copy(newModel);
            copied.id = 100;

            $httpBackend.expectDELETE('/api/people/100').respond(200, '');

            copied.$destroy();
            $httpBackend.flush();
        });

        it('calling $destroy on a model list entry should not have any effect on the list', function(){

            var modelList = new PersonModel.List([
                {
                    id: 1,
                    name: 'Juri'
                },
                {
                    id: 2,
                    name: 'Austin'
                },
                {
                    id: 3,
                    name: 'Tim'
                }
            ]);

            var copy = modelList[1].$copy(); // angular.copy(modelList[1]);

            $httpBackend.expectDELETE('/api/people/2').respond(200,'');

            copy.$destroy();
            $httpBackend.flush();

            expect(modelList.length).toEqual(3);
        });

    });

    describe('when calling $diff', function() {
        // Need to use `JSON.parse(JSON.stringify(...))` to clean up `deep-diff` array
        function toPlainObject(value) {
            return JSON.parse(JSON.stringify(value));
        }

        it('should return differences when model changed', function(){
            var model = new PersonModel({
                id: 1,
                name: 'Juri'
            });

            delete model.id;

            model.name = 'Marat';

            expect(toPlainObject(model.$diff())).toEqual([{
                kind: 'D',
                path: ['id'],
                lhs: 1
            }, {
                kind: 'E',
                path: ['name'],
                lhs: 'Juri',
                rhs: 'Marat'
            }]);
        });
    });
});


================================================
FILE: test/spec/modelUsage.spec.js
================================================
/// <reference path="../../typings/jasmine/jasmine.d.ts"/>
'use strict';

/*
    High level unit/acceptance tests that
    simulate the usage of modelFactory from the perspective
    of a developer/library user, without testing
    the inner workings of the modelFactory service.
*/

describe('A person model defined using modelFactory', function() {
    var PersonModel, PersonWithMapModel;


    beforeEach(angular.mock.module('modelFactory'));

    describe('with the default configuration', function() {

        beforeEach(function() {
            angular.module('test-module', ['modelFactory'])
                .factory('PersonModel', function($modelFactory) {
                    return $modelFactory('/api/people', {
                        actions: {
                            '$customDelete': {
                                method: 'DELETE',
                                url: 'customDelete/{id}'
                            }
                        }
                    });
                })
                .factory('PersonWithMapModel', function($modelFactory) {
                    return $modelFactory('/api/peoplemodified', {
                        pk: 'fooId'
                    });
                });
        });

        beforeEach(angular.mock.module('test-module'));

        beforeEach(inject(function(_PersonModel_, _PersonWithMapModel_) {
            PersonModel = _PersonModel_;
            PersonWithMapModel = _PersonWithMapModel_;
        }));

        describe('when creating a new instance using the "new" keyword', function() {
            var theModel;

            beforeEach(function() {
                theModel = new PersonModel();
            });

            it('we should get a proper instance', function() {
                expect(theModel).toBeDefined();
            });

            it('should have a $save function', function() {
                expect(theModel.$save).toBeDefined();
            });

        });

        describe('when using the list helper', function() {
            var modelList;

            beforeEach(function() {
                modelList = new PersonModel.List([{
                    name: 'Juri'
                }]);
            });

            it('should instantiate a new model list with some predefined objects', function() {
                expect(modelList).toBeDefined();
                expect(modelList.length).toEqual(1);
            });

            it('should contain wrapped model objects', function() {
                expect(modelList[0] instanceof PersonModel).toBeTruthy();
            });

            // TODO this doesn't work right now...should it??
            it('should wrap newly added JavaScript objects', function() {
                modelList.push({
                    name: 'Tom'
                });

                expect(modelList[1] instanceof PersonModel).toBeTruthy();
            });

            it('should account for Array.push(obj1, obj2,...) API; all passed obj should be wrapped as models', function() {
                var newList = new PersonModel.List();

                // act
                newList.push({
                    name: 'Juri'
                }, {
                    name: 'Austin'
                });

                // assert
                expect(newList.length).toEqual(2);
                expect(newList[0] instanceof PersonModel).toBeTruthy();
                expect(newList[1] instanceof PersonModel).toBeTruthy();
            });

            it('should allow to define an empty list', function() {
                var newEmptyList = new PersonModel.List();
                expect(newEmptyList).toBeDefined();
                expect(newEmptyList.length).toEqual(0);
            });

            it('should allow to add elements on a previously empty model list collection', function() {
                var newList = new PersonModel.List();

                newList.push({
                    name: 'Juri'
                });
                expect(newList.length).toEqual(1);
                expect(newList[0] instanceof PersonModel).toBeTruthy(); // wrapping should still work
            });

            it('should allow to add new models', function() {

                modelList.push({
                    name: 'Anna'
                });

                expect(modelList.length).toEqual(2);
            });

        });

        describe('when calling query()', function() {
            var $httpBackend,
                backendListResponse;

            beforeEach(inject(function(_$httpBackend_) {
                $httpBackend = _$httpBackend_;

                backendListResponse = [{
                    name: 'Juri'
                }, {
                    name: 'Jack'
                }, {
                    name: 'Anne'
                }];
            }));

            afterEach(function() {
                $httpBackend.verifyNoOutstandingExpectation();
                $httpBackend.verifyNoOutstandingRequest();
            });

            it('should return a list of people', function() {
                PersonModel.query()
                    .then(function(peopleList) {

                        expect(peopleList).toBeDefined();
                        expect(peopleList.length).toEqual(3);

                    });

                $httpBackend.expectGET('/api/people').respond(200, backendListResponse);
                $httpBackend.flush();
            });

            it('should properly send parameters', function() {
                PersonModel.query({
                    name: 'Juri',
                    age: 29
                });

                $httpBackend.expectGET('/api/people?age=29&name=Juri').respond(200, backendListResponse);
                $httpBackend.flush();
            });

        });

        describe('when calling get(..)', function() {
            var $httpBackend;

            beforeEach(inject(function(_$httpBackend_) {
                $httpBackend = _$httpBackend_;

                $httpBackend
                    .whenGET('/api/people/123')
                    .respond({
                        id: 123,
                        name: 'Juri'
                    });
            }));

            afterEach(function() {
                $httpBackend.verifyNoOutstandingExpectation();
                $httpBackend.verifyNoOutstandingRequest();
            });


            it('should return the requested resource by its id (as number)', function() {
                PersonModel.get(123)
                    .then(function(theFetchedPerson) {
                        expect(theFetchedPerson).toBeDefined();
                        expect(theFetchedPerson.name).toEqual('Juri');
                    });

                $httpBackend.expectGET('/api/people/123');
                $httpBackend.flush();
            });

            it('should return the requested resource by its id (as string)', function() {
                PersonModel.get('123')
                    .then(function(theFetchedPerson) {
                        expect(theFetchedPerson).toBeDefined();
                        expect(theFetchedPerson.name).toEqual('Juri');
                    });

                $httpBackend.expectGET('/api/people/123');
                $httpBackend.flush();
            });

            it('should allow to add additional query params', function(){
                PersonModel.get('123', { age: 29 });

                $httpBackend.expectGET('/api/people/123?age=29').respond({
                    id: 1,
                    age: 29
                });
                $httpBackend.flush();
            });

            xit('should return the requested resource by its id when passing it as object', function() {
                PersonModel.get({
                    id: 123
                });

                $httpBackend.expectGET('/api/people/123');
                $httpBackend.flush();
            });

        });

        describe('when calling $update()', function() {

            it('should update the existing model properties with the new ones', function() {
                var newModel = new PersonModel({
                    name: null
                });

                var newModelUpdate = new PersonModel({
                    name: 'elec29a',
                    language: {
                      de: 'hallo'
                    }
                });

                //act
                newModel.$update(newModelUpdate);

                expect(newModel.name).toEqual('elec29a');
                expect(newModel.language).toBeDefined();
                expect(newModel.language.de).toEqual('hallo');
            });

        });

        describe('when calling $save()', function() {
            var $httpBackend;

            beforeEach(inject(function(_$httpBackend_) {
                $httpBackend = _$httpBackend_;
            }));

            afterEach(function() {
                $httpBackend.verifyNoOutstandingExpectation();
                $httpBackend.verifyNoOutstandingRequest();
            });

            it('should execute a POST when we have a new model', function() {
                var newModel = new PersonModel({
                    name: 'Juri'
                });

                $httpBackend.expectPOST('/api/people', JSON.stringify(newModel)).respond(200, '');

                // act
                newModel.$save();
                $httpBackend.flush();
            });

            it('should execute a PUT when we have an existing model', function() {
                var newModel = new PersonModel({
                    id: 123,
                    name: 'Juri'
                });

                $httpBackend.expectPUT('/api/people/123', JSON.stringify(newModel)).respond(200, '');

                // act
                newModel.$save();
                $httpBackend.flush();
            });

            it('should update the entry with the new results from the server', function() {

                var children = {
                    count: 1
                };

                var newModel = new PersonModel({
                    name: 'Juri',
                    kids: children
                });

                $httpBackend.expectPOST('/api/people', JSON.stringify(newModel)).respond(200, JSON.stringify({
                    id: 12,
                    name: 'Juri Strumpflohner',
                    kids: { count: 99 }
                }));

                //act
                newModel.$save();
                $httpBackend.flush();

                expect(newModel.id).toEqual(12);
                expect(newModel.name).toEqual('Juri Strumpflohner');

                // Make sure object references are not lost
                expect(newModel.kids).toBe(children);
                expect(children.count).toEqual(99);
            });

            it('Should overwrite array properties with the returned server version on update', function() {

                // Set PersonModel object with an array property
                var people = [];
                people.push(new PersonModel({
                        name: 'Ryan'
                    }
                ));
                people.push(new PersonModel({
                        name: 'Austin'
                    }
                ));
                var newModel = new PersonModel({
                    friends: people
                });

                // Create a changed array to return which has an extra element
                var sender = people.slice().reverse();
                sender.push(new PersonModel( { name: 'Juri'}));

                $httpBackend.expectPOST('/api/people', JSON.stringify(newModel)).respond(200, JSON.stringify({
                    friends: sender
                }));

                //act
                newModel.$save();
                $httpBackend.flush();

                // Arrays should be exactly as returned
                expect(newModel.friends.length).toBe(3);
                expect(newModel.friends[1].name).toBe('Ryan');
            });

            it('on a copied model it should sent back the copied model data', function(){
                var newModel = new PersonModel({
                    name: 'Juri'
                });

                var copied = angular.copy(newModel);
                copied.name = 'Austin'; //change something in the clone

                $httpBackend.expectPOST('/api/people', JSON.stringify(copied)).respond(200, '');


                copied.$save();
                $httpBackend.flush();
            });

            it('should not loose $$array reference when updating existing model', function (){
                var list = new PersonModel.List([
                    {
                        id: 1,
                        name: 'Juri'
                    }
                ]);

                var aPerson = new PersonModel({
                    name: 'Jack'
                });

                aPerson.$save()
                    .then(function() {
                        // add to list
                        list.push(aPerson);
                    });
                $httpBackend.expectPOST('/api/people').respond(200, JSON.stringify({ id: 123, name: 'Jack' }));
                $httpBackend.flush();

                // save again
                aPerson.$save();
                $httpBackend.expectPUT('/api/people/123').respond(200, JSON.stringify({ id: 123, name: 'Jack'}));
                $httpBackend.flush();

                // now delete
                aPerson.$destroy();
                $httpBackend.expectDELETE('/api/people/123').respond(200, '');
                $httpBackend.flush();

                expect(list.length).toBe(1);
            });

        });

        describe('when calling $rollback', function() {

            it('should revert to the previous values of the object', function() {
                var newModel = new PersonModel({
                    name: 'Juri'
                });

                // act
                newModel.name = 'Jack';
                newModel.$rollback();

                expect(newModel.name).toEqual('Juri');
            });

            xit('should NOT revert to the old values after an entity has been persisted with $save', inject(function($httpBackend) {
                var newModel = new PersonModel({
                    name: 'Juri'
                });

                newModel.name = 'Jack';

                // persist it
                newModel.$save();

                $httpBackend
                    .expectPOST('/api/people')
                    .respond(200, JSON.stringify({
                        id: 1,
                        name: 'Jack'
                    }));
                $httpBackend.flush();

                // act
                newModel.$rollback();

                // assert
                expect(newModel.name).toEqual('Jack'); // there is nothing to revert 'cause the model is fresh from the server'
            }));

        });

        describe('when deleting an object', function() {
            var $httpBackend;

            beforeEach(inject(function(_$httpBackend_) {
                $httpBackend = _$httpBackend_;
            }));

            afterEach(function() {
                $httpBackend.verifyNoOutstandingExpectation();
                $httpBackend.verifyNoOutstandingRequest();
            });


            it('should properly execute a DELETE request', function() {
                var theModel = new PersonModel({
                    id: 1234
                });

                // act
                theModel.$destroy();

                $httpBackend.expectDELETE('/api/people/1234').respond(200, '');
                $httpBackend.flush();
            });

            it('should not include any data in the request body', function(){
                var theModel = new PersonModel({
                    id: 1234,
                    name: 'Juri',
                    age: 30
                });

                // act
                theModel.$destroy();

                $httpBackend.expect('DELETE', '/api/people/1234', null).respond(200, '');
                $httpBackend.flush();
            });

            it('should not include any data in the request body for custom endpoints', function(){
                var theModel = new PersonModel({
                    id: 1234,
                    name: 'Juri',
                    age: 30
                });

                // act
                theModel.$customDelete();

                $httpBackend.expect('DELETE', '/api/people/customDelete/1234', null).respond(200, '');
                $httpBackend.flush();
            });

            it('should properly execute a correct DELETE request with a different PK name', function(){
                var theModel = new PersonWithMapModel({
                    fooId: 1234
                });

                // act
                theModel.$destroy();

                $httpBackend.expectDELETE('/api/peoplemodified/1234').respond(200, '');
                $httpBackend.flush();
            });

            it('should remove the deleted object from a model list when the deletion succeeds', function() {
                var modelList = new PersonModel.List([{
                    id: 1,
                    name: 'Juri'
                }, {
                    id: 2,
                    name: 'Jack'
                }, {
                    id: 3,
                    name: 'Austin'
                }]);

                // act
                var modelToDelete = modelList[1];
                modelToDelete.$destroy();

                // assert
                $httpBackend.expectDELETE('/api/people/2').respond(200, '');
                $httpBackend.flush();

                expect(modelList.length).toEqual(2);
                expect(modelList[0].id).toEqual(1);
                expect(modelList[1].id).toEqual(3);
            });

            it('should remove the deleted object even if the list order changed', function() {
                var modelList = new PersonModel.List([{
                    id: 1,
                    name: 'Juri'
                }, {
                    id: 2,
                    name: 'Otto'
                }, {
                    id: 3,
                    name: 'Austin'
                }]);

                var modelToDelete = modelList[1];

                // resort the list s.t. the array indices change..
                modelList.sort(function(a, b){
                    return a.name.localeCompare(b.name);
                });

                // act
                modelToDelete.$destroy();

                // should still delete "Otto"
                $httpBackend.expectDELETE('/api/people/2').respond(200, '');
                $httpBackend.flush();

                expect(modelList.length).toEqual(2);
            });

            it('should also properly remove an object that has just been added to the list before', function(){

                var modelList = new PersonModel.List([{
                    id: 1,
                    name: 'Juri'
                }]);


                // save a new model through the $save function
                var newModel = new PersonModel({ name: 'Tom' });
                newModel.$save()
                    .then(function(){
                        // add it to the overall collection
                        modelList.push(newModel);

                        // act: delete the newly added model again
                        modelList[1].$destroy();
                    });

                $httpBackend.expectPOST('/api/people').respond(200, JSON.stringify({ id: 111, name: 'Tom'}));
                $httpBackend.expectDELETE('/api/people/111').respond(200, '');
                $httpBackend.flush();

                // I'd expect that it is properly removed from it
                expect(modelList.length).toBe(1);
                expect(modelList[0].name).toEqual('Juri');
            });

            it('should also remove deleted models that have a different PK name', function(){
                var modelList = new PersonWithMapModel.List([{
                    fooId: 112,
                    name: 'Juri'
                }]);

                //act
                modelList[0].$destroy();

                $httpBackend.expectDELETE('/api/peoplemodified/112').respond(200, '');
                $httpBackend.flush();

                expect(modelList.length).toBe(0);
            });

            it('should NOT remove the deleted object from a model list when the deletion fails', function() {
                var modelList = new PersonModel.List([{
                    id: 1,
                    name: 'Juri'
                }, {
                    id: 2,
                    name: 'Jack'
                }, {
                    id: 3,
                    name: 'Austin'
                }]);

                // act
                modelList[1].$destroy();


                $httpBackend.expectDELETE('/api/people/2').respond(500, '');
                $httpBackend.flush();

                expect(modelList.length).toEqual(3);
            });

        });

    });

    describe('with defaults', function() {

        beforeEach(function() {
            angular.module('test-module', ['modelFactory'])
                .factory('PersonModel', function($modelFactory) {
                    return $modelFactory('/api/people', {
                        defaults: {
                            age: 18 //stupid example I know :)
                        }
                    });
                });
        });

        beforeEach(angular.mock.module('test-module'));

        beforeEach(inject(function(_PersonModel_) {
            PersonModel = _PersonModel_;
        }));

        it('should have them properly set when instantiating a new empty object', function() {
            var personWithDefaults = new PersonModel();

            expect(personWithDefaults.age).toEqual(18);
        });

        it('should use the defaults when creating an object with some data', function() {
            var personWithDefaults = new PersonModel({
                name: 'Juri'
            });

            expect(personWithDefaults.age).toEqual(18);
        });

        it('should set the defaults when creating a list', function() {
            var personWithDefaultsList = new PersonModel.List([{
                name: 'Juri'
            }]);

            expect(personWithDefaultsList[0].age).toEqual(18);
        });

        it('should not overwrite with the default when passing a value for it', function() {
            var personWithDefaults = new PersonModel({
                name: 'Juri',
                age: 29
            });

            expect(personWithDefaults.age).toEqual(29);
        });

    });

    describe('backend URL resolution', function() {
        var $httpBackend;

        beforeEach(function() {
          angular.module('test-module', ['modelFactory'])
              .factory('PersonModel', function($modelFactory) {
                  return $modelFactory('/api/people', {
                      actions: {

                          // static
                          queryChildren: {
                              url: 'children',
                              isArray: true
                          },

                          getById: {
                              url: 'child/{id}/some/subpath'
                          },

                          getByName: {
                              url: 'child/{name}/some/subpath'
                          },

                          deleteLink: {
                              method: 'DELETE',
                              url: 'path/{id}/links/{linkId}'
                          },

                          // instance function
                          '$serverCopy': {
                              method: 'POST',
                              url: 'copy/{name}'
                          },

                          '$customUpdate': {
                              method: 'PUT',
                              url: 'update/{name}'
                          }

                      }
                  });
              });
        });

        beforeEach(angular.mock.module('test-module'));

        beforeEach(inject(function(_PersonModel_, _$httpBackend_) {
            PersonModel = _PersonModel_;
            $httpBackend = _$httpBackend_;
        }));

        it('should work with GET and id variable', function(){
            PersonModel.getById({ id: 123 });

            $httpBackend.expectGET('/api/people/child/123/some/subpath').respond(200, []);
            $httpBackend.flush();
        });


        it('should properly convert a static DELETE request', function(){
            PersonModel.deleteLink({ id: 123, linkId: '111' });

            $httpBackend.expectDELETE('/api/people/path/123/links/111').respond(200, []);
            $httpBackend.flush();
        });

        it('should work with GET and name variable', function(){
            PersonModel.getByName({ name: 'juri' });

            $httpBackend.expectGET('/api/people/child/juri/some/subpath').respond(200, []);
            $httpBackend.flush();
        });

        it('should work with POST and name variable', function(){
            var person = new PersonModel({
              name: 'juri'
            });

            person.$serverCopy();

            $httpBackend.expectPOST('/api/people/copy/juri').respond(200, []);
            $httpBackend.flush();
        });

        it('should work with PUT and name variable', function(){
            var person = new PersonModel({
              name: 'juri'
            });

            person.$customUpdate();

            $httpBackend.expectPUT('/api/people/update/juri').respond(200, []);
            $httpBackend.flush();
        });

    });

    describe('with custom actions', function() {
        var $httpBackend;

        beforeEach(function() {
            angular.module('test-module', ['modelFactory'])
                .factory('PersonModel', function($modelFactory) {
                    return $modelFactory('/api/people', {
                        actions: {

                            // static
                            queryChildren: {
                                url: 'children',
                                isArray: true
                            },

                            // instance function
                            '$serverCopy': {
                                method: 'POST',
                                url: 'copy'
                            }

                        }
                    });
                });
        });

        beforeEach(angular.mock.module('test-module'));

        beforeEach(inject(function(_PersonModel_, _$httpBackend_) {
            PersonModel = _PersonModel_;
            $httpBackend = _$httpBackend_;
        }));

        it('should correctly call the defined url', function() {
            PersonModel.queryChildren();
            $httpBackend.expectGET('/api/people/children').respond(200, []);
            $httpBackend.flush();
        });

        it('should allow to specify query parameters', function() {

            PersonModel.queryChildren({
                type: 'minor'
            });

            $httpBackend.expectGET('/api/people/children?type=minor').respond(200, '');
            $httpBackend.flush();
        });

        it('should wrap the returned objects', function() {

            PersonModel.queryChildren()
                .then(function(result) {
                    expect(result.length).toBe(1);
                    expect(result[0] instanceof PersonModel).toBeTruthy(); // check whether it's a model
                });

            $httpBackend.expectGET('/api/people/children').respond(200, [{
                type: 'minor',
                name: 'Juri'
            }]);
            $httpBackend.flush();
        });

        it('should correctly invoke the custom model instance function', function() {
            var model = new PersonModel({
                name: 'Juri'
            });

            $httpBackend.expectPOST('/api/people/copy').respond(200, '');
            // act
            model.$serverCopy();
            $httpBackend.flush();
        });

    });

    describe('using the isArray property', function(){
        var AddressModel, $httpBackend;

        beforeEach(function() {
            angular.module('test-module', ['modelFactory'])
                .factory('AddressModel', function($modelFactory) {
                    return $modelFactory('/api/addresses', {
                        actions: {
                            'query': {
                                isArray: false
                            },

                            'myCustomAction': {
                                url: 'customAction',
                                isArray: false
                            }
                        }
                    });
                });
        });

        beforeEach(angular.mock.module('test-module'));

        beforeEach(inject(function(_AddressModel_, _$httpBackend_) {
            AddressModel = _AddressModel_;
            $httpBackend = _$httpBackend_;
        }));

        afterEach(function() {
            $httpBackend.verifyNoOutstandingExpectation();
            $httpBackend.verifyNoOutstandingRequest();
        });

        it('when setting it to false should accept non-array responses', function(){
            AddressModel.query()
                .then(function(result){
                    expect(result.rows.length).toBe(1);
                    expect(result.numRecords).toBe(1);
                });

            $httpBackend.expectGET('/api/addresses').respond(200, { rows: [{ id: 1, street: 'test'}], numRecords: 1 });
            $httpBackend.flush();
        });

        it('when setting it to false on a custom action should accept non-array responses', function(){
            AddressModel.myCustomAction()
                .then(function(result){
                    expect(result.rows.length).toBe(1);
                    expect(result.numRecords).toBe(1);
                });

            $httpBackend.expectGET('/api/addresses/customAction').respond(200, { rows: [{ id: 1, street: 'test'}], numRecords: 1 });
            $httpBackend.flush();
        });

    });

    describe('when backend respond with metadata', function() {
        var StadiumModel, $httpBackend;

        beforeEach(function() {
            angular.module('test-module', ['modelFactory'])
                .factory('StadiumModel', function($modelFactory) {
                    return $modelFactory('/api/stadiums', {
                        actions: {
                            'base': {
                                afterRequest: function(response) {
                                    var transfrom = response.data;
                                    delete response.data;
                                    transfrom.meta = response;
                                    return transfrom;
                                }
                            },
                            'query': {
                                afterRequest: function(response) {
                                    var transfrom = response.data;
                                    transfrom.paginator = response.paginator;
                                    return transfrom;
                                }
                            },
                        }
                    });
                });
        });

        beforeEach(angular.mock.module('test-module'));

        beforeEach(inject(function(_StadiumModel_, _$httpBackend_) {
            StadiumModel = _StadiumModel_;
            $httpBackend = _$httpBackend_;
        }));

        afterEach(function() {
            $httpBackend.verifyNoOutstandingExpectation();
            $httpBackend.verifyNoOutstandingRequest();
        });

        it('when backend respond with pagination ', function() {
            StadiumModel.query()
                .then(function(result) {
                    expect(result.length).toBe(3);
                    expect(result.paginator.limit).toBe(3);
                });

            $httpBackend.expectGET('/api/stadiums').respond(200, {
                'data': [{
                    'title': 'Accusantium rem magni accusantium placeat.'
                }, {
                    'title': 'Maxime ut eum pariatur magni quia iusto.'
                }, {
                    'title': 'Sapiente perferendis consectetur ut ipsa consectetur.'
                }],
                'paginator': {
                    'totalCount': 30,
                    'totalPage': 10,
                    'currentPage': 1,
                    'limit': 3
                }
            });
            $httpBackend.flush();
        });
        it('when backend respond with metadata ', function() {
            StadiumModel.get(1)
                .then(function(result) {
                    // console.log(result.meta);
                    expect(result.meta.status.code).toBe(1000);
                    // expect(result.paginator.limit).toBe(3);
                });

            $httpBackend.expectGET('/api/stadiums/1').respond(200, {
                'data': {
                    'title': 'Accusantium rem magni accusantium placeat.'
                },
                'status': {
                    'code': 1000
                }
            });
            $httpBackend.flush();
        });

    });

    describe('using the map property',function(){
        var PersonModel, $httpBackend;

         beforeEach(function() {
            angular.module('test-module', ['modelFactory'])
                .factory('PersonModel', function($modelFactory) {
                    return $modelFactory('/api/people',{
                        map:{
                            'id' : 'personId',
                            'address' :'street'
                        }
                    });
                });
        });

        beforeEach(angular.mock.module('test-module'));

        beforeEach(inject(function(_PersonModel_,_$httpBackend_) {
            PersonModel = _PersonModel_;
            $httpBackend = _$httpBackend_;
        }));

        afterEach(function() {
            $httpBackend.verifyNoOutstandingExpectation();
            $httpBackend.verifyNoOutstandingRequest();
        });

        it('setting map, transpose `personId` to `id` and `street` to `address` on our instance', function(){
            PersonModel.query()
                .then(function(result){
                    expect(result[0].id).toBe(1);
                    expect(result[0].address).toBe('test');
                });

            $httpBackend.expectGET('/api/people').respond(200, [{ personId: 1, street: 'test'}]);
            $httpBackend.flush();
        });

    });

    describe('when the server returns an error response',function(){
        var PersonModel, $httpBackend;

         beforeEach(function() {
            angular.module('test-module', ['modelFactory'])
                .factory('PersonModel', function($modelFactory) {
                    return $modelFactory('/api/people');
                });
        });

        beforeEach(angular.mock.module('test-module'));

        beforeEach(inject(function(_PersonModel_,_$httpBackend_) {
            PersonModel = _PersonModel_;
            $httpBackend = _$httpBackend_;
        }));

        afterEach(function() {
            $httpBackend.verifyNoOutstandingExpectation();
            $httpBackend.verifyNoOutstandingRequest();
        });

        it('should not call the success callback', function(){
            PersonModel.query()
                .then(function(){
                    // this should never be executed
                    expect(false).toBeTruthy();
                });

            $httpBackend.expectGET('/api/people').respond(500);
            $httpBackend.flush();
        });

        it('should be passed to the error promise', function(){
            PersonModel.query()
                .then(function(){
                })
                .catch(function(response){
                    expect(response).toBeDefined();
                    expect(response.status).toEqual(500);
                });

            $httpBackend.expectGET('/api/people').respond(500);
            $httpBackend.flush();
        });

        it('should be possible to use the error callback instead of catch', function(){
            PersonModel.query()
                .then(function(){
                }, function(response){
                    expect(response).toBeDefined();
                    expect(response.status).toEqual(500);
                });

            $httpBackend.expectGET('/api/people').respond(500);
            $httpBackend.flush();
        });

    });

    describe('using the model instance features',function(){
        var Contact, Phone, Address;

         beforeEach(function() {
            angular.module('test-module', ['modelFactory'])
                .factory('Contact', function($modelFactory, Address) {
                    var model = $modelFactory('contacts', {
                        pk: 'Id',
                        map: {
                            Addresses: Address.List
                        },
                        defaults: {
                            GivenName: '',
                            FamilyName: '',
                            Addresses: []
                        },
                        instance: {
                            addAddress: function (options) {
                                this.Addresses.push(new Address(options));
                            },
                            removeAddress: function (address) {
                                this.Addresses.splice(this.Addresses.indexOf(address), 1);
                            }
                        }
                    });

                    return model;
                })
                .factory('Address', function ($modelFactory, Phone) {
                    var model = $modelFactory('Address', {
                        pk: 'Id',
                        map: {
                            Phones: Phone.List
                        },
                        defaults: {
                            Line1: '',
                            Phones: []
                        },
                        instance: {
                            addPhone: function (options) {
                                this.Phones.push(new Phone(options));
                            },
                            removePhone: function (phone) {
                                this.Phones.splice(this.Phones.indexOf(phone), 1);
                            }
                        }
                    });

                    return model;
                })
                .factory('Phone', function($modelFactory){
                    return $modelFactory('', {
                        pk: 'Id',
                        defaults: {
                            Type: 'Mobile',
                            PhoneCode: '1',
                            Number: '',
                            Extension: '',
                            IsPrimary: false
                        }
                    });
                });
        });

        beforeEach(angular.mock.module('test-module'));

        beforeEach(inject(function(_Contact_, _Phone_, _Address_) {
            Contact = _Contact_;
            Phone = _Phone_;
            Address = _Address_;
        }));

        it('should work with addresses',function(){
            var address = new Address();
            expect(address).toBeDefined();

            address.addPhone();

            var anotherAddress = new Address();
            anotherAddress.addPhone();

            expect(address.Phones.length).toEqual(1);
            expect(anotherAddress.Phones.length).toEqual(1);
        });

        it('multiple nesting of Models and Model collections through map', function(){
            var contact = new Contact();

            contact.addAddress({ Line1: '123 Main St'});

            expect(contact.Addresses.length).toEqual(1);
            expect(contact.Addresses[0].Line1).toEqual('123 Main St');
            expect(contact.Addresses[0].Phones.length).toEqual(0);
        });

    });

    describe('when afterRequest returns data', function () {
        var DummyGetModel, $httpBackend;
        beforeEach(function () {
            angular.module('test-module', ['modelFactory'])
                .factory('DummyGetModel', function ($modelFactory) {
                    return $modelFactory('/test/get/afterRequestTest', {
                        actions: {
                            get: {
                                wrap:false,
                                afterRequest: function (res) {
                                    return res.data.newData;
                                }
                            }
                        }
                    });
                });
        });

        beforeEach(angular.mock.module('test-module'));

        beforeEach(inject(function (_DummyGetModel_, _$httpBackend_) {
            DummyGetModel = _DummyGetModel_;
            $httpBackend = _$httpBackend_;
        }));

        it('should reset data if returns null', function(){
            DummyGetModel.get(1).then(function(data){
               expect(data).toBeNull();
            });

            $httpBackend.expectGET('/test/get/afterRequestTest/1').respond(200, {
                'data': {
                    newData: null
                }
            });
            $httpBackend.flush();
        });

        it('should reset data if returns 0', function(){
            DummyGetModel.get(1).then(function(data){
                expect(data).toBe(0);
            });

            $httpBackend.expectGET('/test/get/afterRequestTest/1').respond(200, {
                'data': {
                    newData: 0
                }
            });
            $httpBackend.flush();
        });

        it('should reset data if returns ""', function(){
            DummyGetModel.get(1).then(function(data){
                expect(data).toBe('');
            });

            $httpBackend.expectGET('/test/get/afterRequestTest/1').respond(200, {
                'data': {
                    newData: ''
                }
            });
            $httpBackend.flush();
        });

        it('should not reset data if returns undefined', function(){
            DummyGetModel.get(1).then(function(data){
                expect(data.newData).toBe();
            });

            $httpBackend.expectGET('/test/get/afterRequestTest/1').respond(200, {
                'data': {
                    newData: undefined
                }
            });
            $httpBackend.flush();
        });
    });


    describe('when action url (or action template url) contains slash', function () {
        var DummyGetModel, $httpBackend;
        beforeEach(function () {
            angular.module('test-module', ['modelFactory'])
                .factory('DummyGetModel', function ($modelFactory) {
                    return $modelFactory('/test/get/actionSlashTest', {
                        actions: {
                            get: {
                                url: '{/templateParam,templateParam2}'
                            },
                            query: {
                                url: '/simpleUrl'
                            }
                        }
                    });
                });
        });

        beforeEach(angular.mock.module('test-module'));

        beforeEach(inject(function (_DummyGetModel_, _$httpBackend_) {
            DummyGetModel = _DummyGetModel_;
            $httpBackend = _$httpBackend_;
        }));

        it('should build query with template params and no duplicate slashes', function () {
            DummyGetModel.get({
                templateParam: '123',
                templateParam2: '456'
            });

            $httpBackend.expectGET('/test/get/actionSlashTest/123/456').respond(200);
            $httpBackend.flush();
        });

        it('should set pk as query param, if we have path params as template', function () {
            DummyGetModel.get({
                templateParam: '123',
                templateParam2: '456',
                id: 0
            });

            $httpBackend.expectGET('/test/get/actionSlashTest/123/456?id=0').respond(200);
            $httpBackend.flush();
        });

        it('should query data without duplicate slash', function () {
            DummyGetModel.query();

            $httpBackend.expectGET('/test/get/actionSlashTest/simpleUrl').respond(200);
            $httpBackend.flush();
        });

        it('should query data without duplicate slash and add query params', function () {
            DummyGetModel.query({
                query: 'param'
            });

            $httpBackend.expectGET('/test/get/actionSlashTest/simpleUrl?query=param').respond(200);
            $httpBackend.flush();
        });
    });

    describe('regression test',function(){
        var Contact, Phone, Address;

         beforeEach(function() {
            angular.module('test-module', ['modelFactory'])
                .factory('Contact', function($modelFactory, Address) {
                    var model = $modelFactory('contacts', {
                        pk: 'Id',
                        map: {
                            Addresses: Address.List
                        },
                        defaults: {
                            GivenName: '',
                            FamilyName: '',
                            Addresses: []
                        },
                        instance: {
                            addAddress: function (options) {
                                this.Addresses.push(new Address(options));
                            },
                            removeAddress: function (address) {
                                this.Addresses.splice(this.Addresses.indexOf(address), 1);
                            }
                        }
                    });

                    return model;
                })
                .factory('Address', function ($modelFactory, Phone) {
                    var model = $modelFactory('address', {
                        pk: 'Id',
                        map: {
                            Phone: Phone
                        },
                        defaults: {
                            Line1: ''
                        }
                    });

                    return model;
                })
                .factory('Phone', function($modelFactory){
                    return $modelFactory('phone', {
                        pk: 'Id',
                        defaults: {
                            Type: 'Mobile',
                            PhoneCode: '1',
                            Number: '',
                            Extension: '',
                            IsPrimary: false
                        }
                    });
                });
        });

        beforeEach(angular.mock.module('test-module'));

        beforeEach(inject(function(_Contact_, _Phone_, _Address_) {
            Contact = _Contact_;
            Phone = _Phone_;
            Address = _Address_;
        }));

        it('should work with addresses',function(){
            var contact = new Contact();

            contact.addAddress({
                Line1: '123 Main St',
                Phone: {
                    Number: '112233'
                }
            });

            expect(contact.Addresses.length).toEqual(1);
            expect(contact.Addresses[0].Line1).toEqual('123 Main St');
            expect(contact.Addresses[0].Phone.Number).toEqual('112233');
        });

    });

});


================================================
FILE: test/spec/regression.spec.js
================================================
'use strict';

/*
  Regression tests that emerge from GitHub issues. Their main purpose is to identify
  a bug and make sure it won't be introduced any more
*/

describe('A person model defined using modelFactory', function() {
    var Department;
    var $httpBackend;

    beforeEach(angular.mock.module('modelFactory'));

    beforeEach(function() {
        angular.module('test-module', ['modelFactory'])
            .factory('Department', function($modelFactory) {
                var model = $modelFactory('department', {
                    pk: 'ID',

                    actions: {
                        // base: {
                        //     override: true
                        // },
                        'lookup': {
                            url: 'Lookups',
                            cache: true,
                            method: 'GET',
                            params: {
                                Range: 'All'
                            }
                        }
                    }
                });

                return model;
            });
    });

    beforeEach(angular.mock.module('test-module'));

    beforeEach(inject(function(_$httpBackend_, _Department_) {
        $httpBackend = _$httpBackend_;
        Department = _Department_;
    }));

    afterEach(function() {
        $httpBackend.verifyNoOutstandingExpectation();
        $httpBackend.verifyNoOutstandingRequest();
    });

    it('should correctly override the defaults with the passed data', function(){
        var customParam = 'test';

        $httpBackend.expectGET('department/Lookups?Range=' + customParam).respond(200, {
            id: 1,
            name: 'Human resources'
        });

        Department.lookup({ Range: customParam });

        $httpBackend.flush();
    });

});
Download .txt
gitextract_esd4rmsq/

├── .gitignore
├── .npmignore
├── .travis.yml
├── CONTRIBUTING.md
├── Gruntfile.js
├── LICENSE
├── README.md
├── bower.json
├── examples/
│   ├── plain/
│   │   └── index.html
│   └── simple.js
├── package.json
├── src/
│   └── modelFactory.js
└── test/
    ├── .jshintrc
    ├── karma.conf-ci.js
    ├── karma.conf.js
    └── spec/
        ├── modelFactory.spec.js
        ├── modelUsage.spec.js
        └── regression.spec.js
Download .txt
SYMBOL INDEX (3 symbols across 3 files)

FILE: examples/simple.js
  function Animal (line 6) | function Animal(val) {

FILE: src/modelFactory.js
  function modelFactory (line 266) | function modelFactory(url, options) {

FILE: test/spec/modelFactory.spec.js
  function toPlainObject (line 120) | function toPlainObject(value) {
Condensed preview — 18 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (114K chars).
[
  {
    "path": ".gitignore",
    "chars": 83,
    "preview": "node_modules/\nbower_components/\njspm_packages/\n.idea/*\n_site/\ntypings/\ndist/\n*.log\n"
  },
  {
    "path": ".npmignore",
    "chars": 52,
    "preview": "node_modules/\nbower_components/\ntest/\nsrc/\nexamples/"
  },
  {
    "path": ".travis.yml",
    "chars": 348,
    "preview": "sudo: false\nlanguage: node_js\ncache:\n  directories:\n    - node_modules\nbranches:\n  only:\n    - master\nnotifications:\n  e"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 3822,
    "preview": "Contributing\n============\n\nWe'd love to get contributions from your part...in the end that's the value behind sharing, r"
  },
  {
    "path": "Gruntfile.js",
    "chars": 2877,
    "preview": "module.exports = function(grunt) {\n\n    grunt.loadNpmTasks('grunt-contrib-concat');\n    grunt.loadNpmTasks('grunt-contri"
  },
  {
    "path": "LICENSE",
    "chars": 1068,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2014 \n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 5784,
    "preview": "# modelFactory \n\n[![Build Status](https://travis-ci.org/swimlane/angular-model-factory.svg?branch=master)](https://travi"
  },
  {
    "path": "bower.json",
    "chars": 814,
    "preview": "{\n  \"name\": \"angular-model-factory\",\n  \"authors\": [\n    \"Austin McDaniel <amcdaniel2@gmail.com>\",\n    \"Juri Strumpflohne"
  },
  {
    "path": "examples/plain/index.html",
    "chars": 860,
    "preview": "<!doctype html>\n<html>\n<head>\n</head>\n<body ng-app=\"demo\">\n    <h1>Model Factory demo</h1>\n\n\n\n\n    <script type=\"text/ja"
  },
  {
    "path": "examples/simple.js",
    "chars": 3388,
    "preview": "define(['angular', 'model-factory'], function (angular) {\n\n    var module = angular.module('myapp', ['modelFactory']);\n "
  },
  {
    "path": "package.json",
    "chars": 1268,
    "preview": "{\n  \"name\": \"angular-model-factory\",\n  \"author\": \"Austin McDaniel <amcdaniel2@gmail.com>\",\n  \"authors\": [\n    \"Juri Stru"
  },
  {
    "path": "src/modelFactory.js",
    "chars": 29691,
    "preview": "/* global angular:false */\n'use strict';\n\n(function(global, factory) {\n    if (typeof define === 'function' && define.am"
  },
  {
    "path": "test/.jshintrc",
    "chars": 631,
    "preview": "{\n  \"node\": true,\n  \"browser\": true,\n  \"esnext\": true,\n  \"bitwise\": true,\n  \"camelcase\": true,\n  \"curly\": true,\n  \"eqeqe"
  },
  {
    "path": "test/karma.conf-ci.js",
    "chars": 2386,
    "preview": "// Karma configuration\n// http://karma-runner.github.io/0.12/config/configuration-file.html\n// Generated on 2014-11-27 u"
  },
  {
    "path": "test/karma.conf.js",
    "chars": 2352,
    "preview": "// Karma configuration\n// http://karma-runner.github.io/0.12/config/configuration-file.html\n// Generated on 2014-11-27 u"
  },
  {
    "path": "test/spec/modelFactory.spec.js",
    "chars": 4148,
    "preview": "'use strict';\n\n/*\n  Specs that test the inner workings of the model-factory. Regression\n  tests can be placed in here.\n*"
  },
  {
    "path": "test/spec/modelUsage.spec.js",
    "chars": 48101,
    "preview": "/// <reference path=\"../../typings/jasmine/jasmine.d.ts\"/>\n'use strict';\n\n/*\n    High level unit/acceptance tests that\n "
  },
  {
    "path": "test/spec/regression.spec.js",
    "chars": 1812,
    "preview": "'use strict';\n\n/*\n  Regression tests that emerge from GitHub issues. Their main purpose is to identify\n  a bug and make "
  }
]

About this extraction

This page contains the full source code of the Swimlane/angular-model-factory GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 18 files (106.9 KB), approximately 21.9k tokens, and a symbol index with 3 extracted functions, classes, methods, constants, and types. 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!