Repository: mgonto/restangular
Branch: master
Commit: a81d594f12fc
Files: 18
Total size: 247.6 KB
Directory structure:
gitextract_0_7y_dox/
├── .editorconfig
├── .github/
│ └── ISSUE_TEMPLATE.md
├── .gitignore
├── .jshintrc
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Gruntfile.js
├── MAINTENANCE.md
├── README.md
├── bower.json
├── dist/
│ └── restangular.js
├── karma.conf.js
├── karma.underscore.conf.js
├── license.md
├── package.json
├── src/
│ └── restangular.js
└── test/
└── restangularSpec.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
*Switch to the Preview tab to read this message in the Github issue form.*
The project lacks resources to answer questions regarding general usage and problems occurring from misconfiguration and or improper use. That's why we kindly ask you to following the following guidelines when considering opening an issue.
**Before opening an issue:**
* Please **carefully read the README** and make sure that your question isn't already answered in the documentation.
* Please **review [the configuration methods](https://github.com/mgonto/restangular#configuring-restangular) and [methods description](https://github.com/mgonto/restangular#methods-description)** once more.
* Please **search the existing issue list** to see if there's already an issue **open or closed** that solves your problem.
* Please **search for and/or ask your question on [StackOverflow](http://stackoverflow.com/search?q=restangular)**. You will get answers more quickly there than here.
* Please **debug your problem long and hard** enough to make sure the issue is in Restangular and not in your code and/or backend.
**When opening an issue:**
* Please **create a live Plunker** (fork [this Plunker](http://plnkr.co/edit/26Heuv5F6hUgxpNWNTee?p=info)) demonstrating the problem, use [httpbin](https://httpbin.org/) or [JSONPlaceholder](https://jsonplaceholder.typicode.com/) or similar as backend if needed.
* Please **write your issue in a clear and succint manner**, so that all the details of your issue are easy to understand and fully explained.
* Please **enclose your code in [fenced blocks](https://help.github.com/articles/creating-and-highlighting-code-blocks/)** for readability.
The maintainers may opt to close issues that don't follow these guidelines.
**Also remember:**
* Pull requests fixing problems are highly appreciated ;)
================================================
FILE: .gitignore
================================================
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
pids
logs
results
lib
node_modules
components
coverage
bower_components
npm-debug.log
.idea
================================================
FILE: .jshintrc
================================================
{
"boss": true,
"curly": true,
"eqeqeq": true,
"eqnull": true,
"expr": true,
"immed": true,
"indent": 2,
"maxlen": 140,
"noarg": true,
"onevar": true,
"quotmark": "single",
"smarttabs": true,
"trailing": true,
"undef": true,
"unused": true,
"browser": true,
"globals": {
"module": true,
"angular": true,
"_": true
}
}
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- "7.0"
before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
- 'npm install -g bower grunt-cli'
- 'bower install --config.interactive=false'
script: grunt travis
================================================
FILE: CHANGELOG.md
================================================
## 1.6.1 (2017-01-06)
### Bug fixes
* fix(copy) Collections are now copied/cloned properly ([c92b138](https://github.com/mgonto/restangular/commit/c92b138))
* fix(copy) Copying collections now correctly sets route, fromServer and parent on the copy ([7fd668b](https://github.com/mgonto/restangular/commit/7fd668b))
* fix(elementTransformer) matchTransformer now doesn't throw if route is undefined ([fb242ae](https://github.com/mgonto/restangular/commit/fb242ae))
### Docs
* chore(docs): update contribution guidelines ([c49ca45](https://github.com/mgonto/restangular/commit/c49ca45))
* fix(docs): add link to david-dm.org from badge ([2bfb745](https://github.com/mgonto/restangular/commit/2bfb745))
* chore(docs) Add new example production site ([2596035](https://github.com/mgonto/restangular/commit/2596035))
* chore(docs) Add note about pull requests and github preview tab ([6883075](https://github.com/mgonto/restangular/commit/6883075))
* chore(docs) Apply automatic formatting to code and spec ([bc16122](https://github.com/mgonto/restangular/commit/bc16122))
* chore(docs) Reformat changelog, add unreleased section ([8bfa685](https://github.com/mgonto/restangular/commit/8bfa685))
* chore(docs) Update issue guidelines to include StackOverflow as source for solutions to problems ([34b0e9a](https://github.com/mgonto/restangular/commit/34b0e9a))
* chore(docs) Update link to demo Plunker, rephrase ([7c30615](https://github.com/mgonto/restangular/commit/7c30615))
* chore(test) fix jshint errors in spec file ([1a988cb](https://github.com/mgonto/restangular/commit/1a988cb))
* feat(docs) Add FAQ about cancelling request ([8552c51](https://github.com/mgonto/restangular/commit/8552c51)), closes [#926](https://github.com/mgonto/restangular/issues/926) [#1145](https://github.com/mgonto/restangular/issues/1145) [#1377](https://github.com/mgonto/restangular/issues/1377) [#1391](https://github.com/mgonto/restangular/issues/1391)
### Other
* chore(changelog): upgrade package and config ([58caacd](https://github.com/mgonto/restangular/commit/58caacd))
* chore(dependencies): Update lodash version to ~4.17.0 as in unit tests ([e0b68a0](https://github.com/mgonto/restangular/commit/e0b68a0))
* chore(deps): upgrade dev dependencies, fix tests (#1450) ([b583197](https://github.com/mgonto/restangular/commit/b583197)), closes [#1450](https://github.com/mgonto/restangular/issues/1450)
* chore(travis): change travis script and include coveralls ([ca9856a](https://github.com/mgonto/restangular/commit/ca9856a))
* test(coverage): add coverage and coveralls.io integration ([fdd5de6](https://github.com/mgonto/restangular/commit/fdd5de6))
* Update dist files ([7c245a2](https://github.com/mgonto/restangular/commit/7c245a2))
## 1.6.0 (2016-12-25)
* Url now supports unescaped suffix (0350bcd)
* Added Restangular Plunkr example (c4ef002)
* Now id can be a nested property (a94228b)
* Add withHttpConfig to objects created with .service (e8f7295)
* Add support for angularjs dependency injection using commonjs require syntax (f02db83)
* Fix missing 'get' in decoupled service (8096ce1)
* Avoid restangularizing an undefined element in restangularizeCollecti onAndElements. (0f8b562)
* Fixes #1167: Extend condition to treat '0, which as a falsy value currently fails, as a valid ID (95ea231)
* Add customPatch method (01297fe)
* Added UMD snippet (caab5e6)
* Support BaseUrl with athority without schema (5f3eacb)
* Add ability to restangularize a collection with fromServer set (51066ec)
* Add configuration option to use plain() by default (94ffaf0)
* Fix fromServer param while copying (b53f4b6)
* Rename CONTRIBUTE.md to CONTRIBUTING.md in accordance with GitHub's spec (c17df47)
* Remove moot `version` property from bower.json (1a585f3)
* Add more realistic POST response for accounts, with id (#943) (11fb475)
* Added context/explanation of when to use JSONP. (fec9b27)
* Add regexp matching for route to element transformers (#1430) (de8f561)
## 1.5.2 (2016-02-15)
* Change \_.contains to \_.includes for compatability with lodash >= 4.0
## 1.5.1 (2015-04-03)
* Release 1.5.0
* Updated zip
* Merge branch 'master' of github.com:mgonto/restangular
* Merge pull request #1081 from rajeshwarpatlolla/develop
* Merge pull request #1079 from wching/master
* change in README file
* url modified for 'Chain methods together to easily build complex requests'
* Update README.md
* Update README.md
## 1.5.0 (2015-04-03)
* Tons of bug fixes
* Upgraded Lodash to 1.3.0
================================================
FILE: CONTRIBUTING.md
================================================
# Issues
Read the [issue guidlines](.github/ISSUE_TEMPLATE.md) before opening an issue!
# How to submit PRs
## Install env
In order to contribute just create your own [fork](https://help.github.com/articles/fork-a-repo/)
of the repository and then run:
```
git clone git@github.com:/restangular.git
cd restangular
npm install grunt-cli --global
npm install
```
## Create a branch
Create a branch for your code changes
`git checkout -b my_awesome_fix`
## Write tests
When making changes to the code, please always **write a test case** for
your topic before making the change. Watch the test fail, then
implement the change and watch the test succeed.
Tests are run with `grunt test`.
## Keep the style
When you're done with your code, run `grunt jshint` to check
if you code follow the same simple coding design as the rest of the project.
Consider integrating `jshint` in your editor to get real time feedback on your
style as you're coding.
## Commit message format
Please write your commit messages in the [angular conventional changelog](https://github.com/conventional-changelog/conventional-changelog-angular/blob/master/convention.md) format. This will help
us to keep a decent [CHANGELOG](CHANGELOG.md) with minimum effort. Check previous commit
messages for examples.
## Update docs
If your code change includes new features, please include an update to the [README.md](README.md)
explaining your feature.
**Don't**, however, generate distribution files (the files in [dist](dist)). This will be done
on a regular basis by maintainers as PRs are merged.
## Squash commits
Please consider squasing the commits in your topic branch into a single commit including
all changes needed. This will make the PR cleaner and the change history more easy to follow
after the PR has been merged. Also, the CHANGELOG will make more sense.
Look [here](https://ariejan.net/2011/07/05/git-squash-your-latests-commits-into-one/) and
[here](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Squashing-Commits) for how to.
## Sumbit the PR
Now you're ready to [open a PR](https://help.github.com/articles/creating-a-pull-request-from-a-fork/).
Thanks!
================================================
FILE: Gruntfile.js
================================================
module.exports = function(grunt) {
'use strict';
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
meta: {
banner: [
'/**',
' * <%= pkg.description %>',
' * @version v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>' +
' * @link <%= pkg.homepage %>',
' * @author <%= pkg.author %>',
' * @license MIT License, http://www.opensource.org/licenses/MIT',
' */'
].join('\n')
},
dirs: {
dest: 'dist'
},
concat: {
options: {
banner: '<%= meta.banner %>'
},
dist: {
src: ['src/*.js'],
dest: '<%= dirs.dest %>/<%= pkg.name %>.js'
}
},
zip: {
'<%= dirs.dest %>/restangular.zip': [
'<%= dirs.dest %>/<%= pkg.name %>.js',
'<%= dirs.dest %>/<%= pkg.name %>.min.js'
]
},
bowerInstall: {
install: {
}
},
uglify: {
options: {
banner: '<%= meta.banner %>'
},
dist: {
src: ['<%= concat.dist.dest %>'],
dest: '<%= dirs.dest %>/<%= pkg.name %>.min.js'
}
},
jshint: {
files: ['Gruntfile.js', 'src/*.js'],
options: {
jshintrc: true
}
},
karma: {
options: {
configFile: 'karma.conf.js'
},
build: {
singleRun: true,
autoWatch: false
},
debug: {
singleRun: false,
autoWatch: true,
browsers: ['Chrome']
},
travis: {
singleRun: true,
autoWatch: false,
browsers: ['Firefox']
},
travisUnderscore: {
singleRun: true,
autoWatch: false,
browsers: ['Firefox'],
configFile: 'karma.underscore.conf.js',
},
buildUnderscore: {
configFile: 'karma.underscore.conf.js',
singleRun: true,
autoWatch: false
},
dev: {
autoWatch: true
}
},
coveralls: {
// Options relevant to all targets
options: {
// When true, grunt-coveralls will only print a warning rather than
// an error, to prevent CI builds from failing unnecessarily (e.g. if
// coveralls.io is down). Optional, defaults to false.
force: false
},
restangular: {
// LCOV coverage file (can be string, glob or array)
src: 'coverage/**/lcov.info',
options: {
// Any options for just this target
}
},
},
conventionalChangelog: {
options: {
changelogOpts: {
// conventional-changelog options go here
outputUnreleased: true,
// preset: 'angular'
},
context: {
// context goes here
},
gitRawCommitsOpts: {
// git-raw-commits options go here
},
parserOpts: {
// conventional-commits-parser options go here
},
writerOpts: {
// conventional-changelog-writer options go here
}
},
release: {
src: 'CHANGELOG.md'
}
}
});
// Load the plugin that provides the "jshint" task.
grunt.loadNpmTasks('grunt-contrib-jshint');
// Load the plugin that provides the "concat" task.
grunt.loadNpmTasks('grunt-contrib-concat');
// Load the plugin that provides the "uglify" task.
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-bower-task');
grunt.renameTask('bower', 'bowerInstall');
grunt.loadNpmTasks('grunt-karma');
grunt.loadNpmTasks('grunt-conventional-changelog');
grunt.loadNpmTasks('grunt-zip');
grunt.loadNpmTasks('grunt-coveralls');
// Default task.
grunt.registerTask('default', ['build']);
// Build task.
grunt.registerTask('build', ['bowerInstall', 'karma:build', 'karma:buildUnderscore', 'concat', 'uglify', 'zip']);
grunt.registerTask('test', ['karma:build', 'karma:buildUnderscore']);
grunt.registerTask('test-debug', ['karma:debug']);
grunt.registerTask('travis', ['karma:travis', 'karma:travisUnderscore', 'coveralls']);
grunt.registerTask('changelog', ['conventionalChangelog']);
// Provides the "bump" task.
grunt.registerTask('bump', 'Increment version number', function() {
var versionType = grunt.option('type');
function bumpVersion(version, versionType) {
var type = {patch: 2, minor: 1, major: 0},
parts = version.split('.'),
idx = type[versionType || 'patch'];
parts[idx] = parseInt(parts[idx], 10) + 1;
while(++idx < parts.length) { parts[idx] = 0; }
return parts.join('.');
}
var version;
function updateFile(file) {
var json = grunt.file.readJSON(file);
version = json.version = bumpVersion(json.version, versionType || 'patch');
grunt.file.write(file, JSON.stringify(json, null, ' '));
}
updateFile('package.json');
grunt.log.ok('Version bumped to ' + version);
});
};
================================================
FILE: MAINTENANCE.md
================================================
# Restangular maintenance policy
Restangular follows the [Semantic Versioning](http://semver.org/) for its releases:
`(Major).(Minor).(Patch)`.
- **Patch number**: When backwards compatible bug fixes are introduced that fix
incorrect behavior.
- **Minor version**: When new, backwards compatible functionality is introduced
to the public API or a minor feature is introduced, or when a set of smaller
features is rolled out.
- **Major version**: Whenever there is something significant or any backwards
incompatible changes are introduced to the public API.
The current stable release will receive security patches and bug fixes
(eg. `1.6.0` -> `1.6.1`). Feature releases will mark the next supported stable
release where the minor version is increased numerically by increments of one
(eg. `1.6.3 -> 1.7.0`).
We encourage everyone to run the latest stable release.
# Notes for maintainers
Following are a set of guidelines and checklists for maintaining the
[main repository](https://github.com/mgonto/restangular).
## Keeping master usable
In order for developers to use the bleeding edge master version in their projects,
we'll need to keep the dist files up to date. After merging PRs that include mentionable
changes, please update the dist files `grunt build` and the CHANGELOG `grunt changelog`.
The CHANGELOG will have a section called `Unreleased` for changes not yet included in
any release. This is the place for
## Releasing
Follow this checklist for publishing a new release.
- [ ] Bump version `grunt bump --type=patch/minor/major`
- [ ] Create changelog `grunt changelog` (open it in editor and make sure its sensible)
- [ ] Make dist files `grunt build`
- [ ] Commit changes `git add dist package.json CHANGELOG.md` and `git commit -m "chore(release): release 1.6.1"`
- [ ] Tag release `git tag -a -m "Version 1.6.1" 1.6.1`
- [ ] Push everything `git push` and `git push --tags`
- [ ] Publish to NPM `npm publish`
- [ ] Create a [new release](https://github.com/mgonto/restangular/releases) on GitHub, entering the version changelog as body
================================================
FILE: README.md
================================================
# Restangular
[](https://travis-ci.org/mgonto/restangular)
[](https://coveralls.io/github/mgonto/restangular?branch=master)
[](https://david-dm.org/mgonto/restangular/?type=dev)
[](https://snyk.io/test/github/mgonto/restangular)
[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=martin%40gon%2eto&lc=US&item_name=Martin%20Gontovnikas¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted "Donate once-off to this project using Paypal")
[](https://www.gittip.com/mgonto/)
Restangular is an AngularJS service that simplifies common GET, POST, DELETE, and UPDATE requests with a minimum of client code. It's a perfect fit for any WebApp that consumes data from a RESTful API.
**Note This version of Restangular [only supports Angular 1](#supported-angular-versions). For an Angular 2+ version of Restangular, check out [ngx-restangular](https://github.com/2muchcoffeecom/ngx-restangular).** It's a separate project with different maintainers, so issues regarding ngx-restangular should be reported [over there](https://github.com/2muchcoffeecom/ngx-restangular/issues) :wink:
Learn Restangular! Try the [live demo on plunkr](http://plnkr.co/edit/8qrGeE?p=preview). It uses the same example as the official [Angular Javascript Project](http://angularjs.org/#wire-up-a-backend), but with Restangular! Or watch [a video introduction of a talk I gave at Devoxx France](http://www.parleys.com/play/535a189ee4b0c5ba17d43455/chapter1/about) about Restangular.
# Table of contents
- [Restangular](#restangular)
- [Differences with $resource](#differences-with-resource)
- [How do I add this to my project?](#how-do-i-add-this-to-my-project)
- [Dependencies](#dependencies)
- [Production apps using Restangular](#production-apps-using-restangular)
- [Starter Guide](#starter-guide)
- [Quick configuration for Lazy Readers](#quick-configuration-for-lazy-readers)
- [Adding dependency to Restangular module in your app](#adding-dependency-to-restangular-module-in-your-app)
- [Using Restangular](#using-restangular)
- [Creating Main Restangular object](#creating-main-restangular-object)
- [Let's code!](#lets-code)
- [Configuring Restangular](#configuring-restangular)
- [Properties](#properties)
- [setBaseUrl](#setbaseurl)
- [setExtraFields](#setextrafields)
- [setParentless](#setparentless)
- [setDefaultHttpFields](#setdefaulthttpfields)
- [addElementTransformer](#addelementtransformer)
- [setOnElemRestangularized](#setonelemrestangularized)
- [setResponseInterceptor](#setresponseinterceptor)
- [setResponseExtractor (alias of setResponseInterceptor)](#setresponseinterceptor)
- [addResponseInterceptor](#addresponseinterceptor)
- [setRequestInterceptor](#setrequestinterceptor)
- [addRequestInterceptor](#addrequestinterceptor)
- [setFullRequestInterceptor](#setfullrequestinterceptor)
- [addFullRequestInterceptor](#addfullrequestinterceptor)
- [setErrorInterceptor](#seterrorinterceptor)
- [setRestangularFields](#setrestangularfields)
- [setMethodOverriders](#setmethodoverriders)
- [setDefaultRequestParams](#setdefaultrequestparams)
- [setFullResponse](#setfullresponse)
- [setDefaultHeaders](#setdefaultheaders)
- [setRequestSuffix](#setrequestsuffix)
- [setUseCannonicalId](#setusecannonicalid)
- [setPlainByDefault](#setplainbydefault)
- [How to configure them globally](#how-to-configure-them-globally)
- [Configuring in the config](#configuring-in-the-config)
- [Configuring in the run](#configuring-in-the-run)
- [How to create a Restangular service with a different configuration from the global one](#how-to-create-a-restangular-service-with-a-different-configuration-from-the-global-one)
- [Decoupled Restangular Service](#decoupled-restangular-service)
- [Methods description](#methods-description)
- [Restangular methods](#restangular-methods)
- [Element methods](#element-methods)
- [Collection methods](#collection-methods)
- [Custom methods](#custom-methods)
- [Copying elements](#copying-elements)
- [Enhanced promises](#enhanced-promises)
- [Using values directly in templates](#using-values-directly-in-templates)
- [Using Self reference resources](#using-self-reference-resources)
- [URL Building](#url-building)
- [Creating new Restangular Methods](#creating-new-restangular-methods)
- [Adding Custom Methods to Collections](#adding-custom-methods-to-collections)
- [Example:](#example)
- [Adding Custom Methods to Models](#adding-custom-methods-to-models)
- [Example:](#example-1)
- [FAQ](#faq)
- [How can I handle errors?](#how-can-i-handle-errors)
- [I need to send one header in EVERY Restangular request, how do I do this?](#i-need-to-send-one-header-in-every-restangular-request-how-do-i-do-this)
- [Can I cache requests?](#can-i-cache-requests)
- [Can it be used in $routeProvider.resolve?](#can-it-be-used-in-routeproviderresolve)
- [My response is actually wrapped with some metadata. How do I get the data in that case?](#my-response-is-actually-wrapped-with-some-metadata-how-do-i-get-the-data-in-that-case)
- [I use Mongo and the ID of the elements is _id not id as the default. Therefore requests are sent to undefined routes](#i-use-mongo-and-the-id-of-the-elements-is-_id-not-id-as-the-default-therefore-requests-are-sent-to-undefined-routes)
- [What if each of my models has a different ID name like CustomerID for Customer](#what-if-each-of-my-models-has-a-different-id-name-like-customerid-for-customer)
- [How do I handle CRUD operations in a List returned by Restangular?](#how-do-i-handle-crud-operations-in-a-list-returned-by-restangular)
- [When I set baseUrl with a port, it's stripped out.](#when-i-set-baseurl-with-a-port-its-stripped-out)
- [How can I access the unrestangularized element as well as the restangularized one?](#how-can-i-access-the-unrestangularized-element-as-well-as-the-restangularized-one)
- [Restangular fails with status code 0](#restangular-fails-with-status-code-0)
- [Why does this depend on Lodash / Underscore?](#why-does-this-depend-on-lodash--underscore)
- [How do I cancel a request?](#how-do-i-cancel-a-request)
- [Supported Angular versions](#supported-angular-versions)
- [Server Frameworks](#server-frameworks)
- [Releases Notes](#releases-notes)
- [License](#license)
**[Back to top](#table-of-contents)**
## Differences with $resource
Restangular has several features that distinguish it from $resource:
* **It uses [promises](http://docs.angularjs.org/api/ng.$q)**. Instead of doing the "magic" filling of objects like $resource, it uses promises.
* **You can use this in $routeProvider.resolve**. As Restangular returns promises, you can return any of the methods in the `$routeProvider.resolve` and you'll get the real object injected into your controller if you want.
* **It doesn't have all those `$resource` bugs**. Restangular doesn't have problem with trailing slashes, additional `:` in the URL, escaping information, expecting only arrays for getting lists, etc.
* **It supports all HTTP methods**.
* **It supports ETag out of the box**. You don't have to do anything. ETags and If-None-Match will be used in all of your requests
* **It supports self linking elements**. If you receive from the server some item that has a link to itself, you can use that to query the server instead of writing the URL manually.
* **You don't have to create one $resource object per request**. Each time you want to do a request, you can just do it using the object that was returned by Restangular. You don't need to create a new object for this.
* **You don't have to write or remember ANY URL**. With $resource, you need to write the URL Template. In here, you don't write any urls. You just write the name of the resource you want to fetch and that's it.
* **It supports nested RESTful resources**. If you have Nested RESTful resources, Restangular can handle them for you. You don't have to know the URL, the path, or anything to do all of the HTTP operations you want.
* **Restangular lets you create your own methods**. You can create your own methods to run the operation that you want. The sky is the limit.
* **Support for wrapped responses**. If your response for a list of element actually returns an object with some property inside which has the list, it's very hard to use $resource. Restangular knows that and it makes it easy on you. Check out https://github.com/mgonto/restangular#my-response-is-actually-wrapped-with-some-metadata-how-do-i-get-the-data-in-that-case
* **You can build your own URLs with Restangular objects easily**. Restangular lets you create a Restangular object for any url you want with a really nice builder.
Let's see a quick and short example of these features
````javascript
// Restangular returns promises
Restangular.all('users').getList() // GET: /users
.then(function(users) {
// returns a list of users
$scope.user = users[0]; // first Restangular obj in list: { id: 123 }
})
// Later in the code...
// Restangular objects are self-aware and know how to make their own RESTful requests
$scope.user.getList('cars'); // GET: /users/123/cars
// You can also use your own custom methods on Restangular objects
$scope.user.sendMessage(); // POST: /users/123/sendMessage
// Chain methods together to easily build complex requests
$scope.user.one('messages', 123).one('from', 123).getList('unread');
// GET: /users/123/messages/123/from/123/unread
````
**[Back to top](#table-of-contents)**
## How do I add this to my project?
You can download this by:
* Using bower and running `bower install restangular`
* Using npm and running `npm install restangular`
* Downloading it manually by clicking [here to download development unminified version](https://raw.github.com/mgonto/restangular/master/dist/restangular.js) or [here to download minified production version](https://raw.github.com/mgonto/restangular/master/dist/restangular.min.js)
* Using [CdnJS CDN files](http://cdnjs.com/libraries/restangular/):
````html
````
**[Back to top](#table-of-contents)**
## Dependencies
Restangular depends on Angular and Lodash (or Underscore).
**[Back to top](#table-of-contents)**
# Production apps using Restangular
Each time, there're more Production WebApps using `Restangular`. If your webapp uses it and it's not in the list, please create an issue or submit a PR:
* **Life360** is using Restangular to build the WebApp version of their platform
* **Thomson Reuters** is using Restangular for the new Webapp they've built
* **Quran.com** is using Restangular for their alpha/beta app and soon to be main site
* **[Worldcampus.co](http://www.worldcampus.co)** is using Restangular for their beta international students social network.
* **[ENTSO-E Transparency Platform](https://transparency.entsoe.eu)**
**[Back to top](#table-of-contents)**
# Starter Guide
## Quick Configuration (For Lazy Readers)
This is all you need to start using all the basic Restangular features.
````javascript
// Add Restangular as a dependency to your app
angular.module('your-app', ['restangular']);
// Inject Restangular into your controller
angular.module('your-app').controller('MainCtrl', function($scope, Restangular) {
// ...
});
````
The Restangular service may be injected into any Controller or Directive :)
Note: When adding Restangular as a dependency it is not capitalized 'restangular'
But when injected into your controller it is 'Restangular'
**[Back to top](#table-of-contents)**
## Using Restangular
### Creating Main Restangular object
There are 3 ways of creating a main Restangular object.
The first one and most common one is by stating the main route of all requests.
The second one is by stating the main route and object of all requests.
````javascript
// Only stating main route
Restangular.all('accounts')
// Stating main object
Restangular.one('accounts', 1234)
// Gets a list of all of those accounts
Restangular.several('accounts', 1234, 123, 12345);
````
**[Back to top](#table-of-contents)**
### Let's code!
Now that we have our main Object let's start playing with it.
````javascript
// First way of creating a Restangular object. Just saying the base URL
var baseAccounts = Restangular.all('accounts');
// This will query /accounts and return a promise.
baseAccounts.getList().then(function(accounts) {
$scope.allAccounts = accounts;
});
// Does a GET to /accounts
// Returns an empty array by default. Once a value is returned from the server
// that array is filled with those values. So you can use this in your template
$scope.accounts = Restangular.all('accounts').getList().$object;
var newAccount = {name: "Gonto's account"};
// POST /accounts
baseAccounts.post(newAccount);
// GET to http://www.google.com/ You set the URL in this case
Restangular.allUrl('googlers', 'http://www.google.com/').getList();
// GET to http://www.google.com/1 You set the URL in this case
Restangular.oneUrl('googlers', 'http://www.google.com/1').get();
// You can do RequestLess "connections" if you need as well
// Just ONE GET to /accounts/123/buildings/456
Restangular.one('accounts', 123).one('buildings', 456).get()
// Just ONE GET to /accounts/123/buildings
Restangular.one('accounts', 123).getList('buildings')
// Here we use Promises then
// GET /accounts
baseAccounts.getList().then(function (accounts) {
// Here we can continue fetching the tree :).
var firstAccount = accounts[0];
// This will query /accounts/123/buildings considering 123 is the id of the firstAccount
$scope.buildings = firstAccount.getList("buildings");
// GET /accounts/123/places?query=param with request header: x-user:mgonto
$scope.loggedInPlaces = firstAccount.getList("places", {query: 'param'}, {x-user: 'mgonto'})
// This is a regular JS object, we can change anything we want :)
firstAccount.name = "Gonto"
// If we wanted to keep the original as it is, we can copy it to a new element
var editFirstAccount = Restangular.copy(firstAccount);
editFirstAccount.name = "New Name";
// PUT /accounts/123. The name of this account will be changed from now on
firstAccount.put();
editFirstAccount.put();
// PUT /accounts/123. Save will do POST or PUT accordingly
firstAccount.save();
// DELETE /accounts/123 We don't have first account anymore :(
firstAccount.remove();
var myBuilding = {
name: "Gonto's Building",
place: "Argentina"
};
// POST /accounts/123/buildings with MyBuilding information
firstAccount.post("Buildings", myBuilding).then(function() {
console.log("Object saved OK");
}, function() {
console.log("There was an error saving");
});
// GET /accounts/123/users?query=params
firstAccount.getList("users", {query: params}).then(function(users) {
// Instead of posting nested element, a collection can post to itself
// POST /accounts/123/users
users.post({userName: 'unknown'});
// Custom methods are available now :).
// GET /accounts/123/users/messages?param=myParam
users.customGET("messages", {param: "myParam"})
var firstUser = users[0];
// GET /accounts/123/users/456. Just in case we want to update one user :)
$scope.userFromServer = firstUser.get();
// ALL http methods are available :)
// HEAD /accounts/123/users/456
firstUser.head()
});
}, function errorCallback() {
alert("Oops error from server :(");
})
// Second way of creating Restangular object. URL and ID :)
var account = Restangular.one("accounts", 123);
// GET /accounts/123?single=true
$scope.account = account.get({single: true});
// POST /accounts/123/messages?param=myParam with the body of name: "My Message"
account.customPOST({name: "My Message"}, "messages", {param: "myParam"}, {})
````
**[Back to top](#table-of-contents)**
## Configuring Restangular
### Properties
Restangular comes with defaults for all of its properties but you can configure them. **So, if you don't need to configure something, there's no need to add the configuration.**
You can set all these configurations in **`RestangularProvider` or `Restangular` service to change the global configuration** or you can **use the withConfig method in Restangular service to create a new Restangular service with some scoped configuration**. Check the section on this later.
#### setBaseUrl
The base URL for all calls to your API. For example if your URL for fetching accounts is http://example.com/api/v1/accounts, then your baseUrl is `/api/v1`. The default baseUrl is an empty string which resolves to the same url that AngularJS is running, but you can also set an absolute url like `http://api.example.com/api/v1` if you need to set another domain.
#### setExtraFields
These are the fields that you want to save from your parent resources if you need to display them. By default this is an Empty Array which will suit most cases
#### setParentless
Use this property to control whether Restangularized elements to have a parent or not. So, for example if you get an account and then get a nested list of buildings, you may want the buildings URL to be simple `/buildings/123` instead of `/accounts/123/buildings/123`. This property lets you do that.
This method accepts 1 parameter, it could be:
* Boolean: Specifies if all elements should be parentless or not
* Array: Specifies the routes (types) of all elements that should be parentless. For example `['buildings']`
#### setDefaultHttpFields
`$http` from AngularJS can receive a bunch of parameters like `cache`, `transformRequest` and so on. You can set all of those properties in the object sent on this setter so that they will be used in EVERY API call made by Restangular. This is very useful for caching for example. All properties that can be set can be checked here: http://docs.angularjs.org/api/ng.$http#parameters
#### addElementTransformer
This is a hook. After each element has been "restangularized" (Added the new methods from Restangular), the corresponding transformer will be called if it fits.
This should be used to add your own methods / functions to entities of certain types.
You can add as many element transformers as you want. The signature of this method can be one of the following:
* **addElementTransformer(route, transformer)**: Transformer is called with all elements that have been restangularized, no matter if they're collections or not.
* **addElementTransformer(route, isCollection, transformer)**: Transformer is called with all elements that have been restangularized and match the specification regarding if it's a collection or not (true | false)
#### setTransformOnlyServerElements
This sets whether transformers will be run for local objects and not by objects returned by the server. This is by default true but can be changed to false if needed (Most people won't need this).
#### setOnElemRestangularized
This is a hook. After each element has been "restangularized" (Added the new methods from Restangular), this will be called. It means that if you receive a list of objects in one call, this method will be called first for the collection and then for each element of the collection.
**I favor the usage of `addElementTransformer` instead of `onElemRestangularized` whenever possible as the implementation is much cleaner.**
This callback is a function that has 4 parameters:
* **elem**: The element that has just been restangularized. Can be a collection or a single element.
* **isCollection**: Boolean indicating if this is a collection or a single element.
* **what**: The model that is being modified. This is the "path" of this resource. For example `buildings`
* **Restangular**: The instanced service to use any of its methods
This can be used together with `addRestangularMethod` (Explained later) to add custom methods to an element
#### setResponseInterceptor
**This is deprecated. Use addResponseInterceptor since you can add more than one**.
#### addResponseInterceptor
The responseInterceptor is called after we get each response from the server. It's a function that receives this arguments:
* **data**: The data received got from the server
* **operation**: The operation made. It'll be the HTTP method used except for a `GET` which returns a list of element which will return `getList` so that you can distinguish them.
* **what**: The model that's being requested. It can be for example: `accounts`, `buildings`, etc.
* **url**: The relative URL being requested. For example: `/api/v1/accounts/123`
* **response**: Full server response including headers
* **deferred**: The deferred promise for the request.
Some of the use cases of the responseInterceptor are handling wrapped responses and enhancing response elements with more methods among others.
The responseInterceptor must return the restangularized data element.
#### setRequestInterceptor
**This is deprecated. Use addRequestInterceptor since you can add more than one**.
#### addRequestInterceptor
The requestInterceptor is called before sending any data to the server. It's a function that must return the element to be requested. This function receives the following arguments:
* **element**: The element to send to the server.
* **operation**: The operation made. It'll be the HTTP method used except for a `GET` which returns a list of element which will return `getList` so that you can distinguish them.
* **what**: The model that's being requested. It can be for example: `accounts`, `buildings`, etc.
* **url**: The relative URL being requested. For example: `/api/v1/accounts/123`
#### setFullRequestInterceptor
**This is deprecated. Use addFullRequestInterceptor since you can add more than one**.
#### addFullRequestInterceptor
This adds a new fullRequestInterceptor. The fullRequestInterceptor is similar to the `requestInterceptor` but more powerful. It lets you change the element, the request parameters and the headers as well.
It's a function that receives the same as the `requestInterceptor` plus the headers and the query parameters (in that order).
It can return an object with any (or all) of following properties:
* **headers**: The headers to send
* **params**: The request parameters to send
* **element**: The element to send
* **httpConfig**: The httpConfig to call with
If a property isn't returned, the one sent is used.
#### setErrorInterceptor
The errorInterceptor is called whenever there's an error. It's a function that receives the response, the deferred (for the promise) and the Restangular-response handler as parameters.
The errorInterceptor function, whenever it returns `false`, prevents the promise linked to a Restangular request to be executed. All other return values (besides `false`) are ignored and the promise follows the usual path, eventually reaching the success or error hooks.
The feature to prevent the promise to complete is useful whenever you need to intercept each Restangular error response for every request in your AngularJS application in a single place, increasing debugging capabilities and hooking security features in a single place.
````javascript
var refreshAccesstoken = function() {
var deferred = $q.defer();
// Refresh access-token logic
return deferred.promise;
};
Restangular.setErrorInterceptor(function(response, deferred, responseHandler) {
if(response.status === 403) {
refreshAccesstoken().then(function() {
// Repeat the request and then call the handlers the usual way.
$http(response.config).then(responseHandler, deferred.reject);
// Be aware that no request interceptors are called this way.
});
return false; // error handled
}
return true; // error not handled
});
````
#### setRestangularFields
Restangular requires 7 fields for every "Restangularized" element. These are:
* id: Id of the element. Default: id
* route: Name of the route of this element. Default: route
* parentResource: The reference to the parent resource. Default: parentResource
* restangularCollection: A boolean indicating if this is a collection or an element. Default: restangularCollection
* cannonicalId: If available, the path to the cannonical ID to use. Useful for PK changes
* etag: Where to save the ETag received from the server. Defaults to `restangularEtag`
* selfLink: The path to the property that has the URL to this item. If your REST API doesn't return a URL to an item, you can just leave it blank. Defaults to `href`
Also all of Restangular methods and functions are configurable through restangularFields property.
All of these fields except for `id` and `selfLink` are handled by Restangular, so most of the time you won't change them. You can configure the name of the property that will be binded to all of this fields by setting restangularFields property.
#### setMethodOverriders
You can now Override HTTP Methods. You can set here the array of methods to override. All those methods will be sent as POST and Restangular will add an X-HTTP-Method-Override header with the real HTTP method we wanted to do.
#### setJsonp
Typical web browsers prohibit requesting data from a server in a different domain (same-origin policy). JSONP or "JSON with padding" is a communication technique used in JavaScript programs running in web browsers to get around this.
For JSONP to work, a server must know how to reply with JSONP-formatted results. JSONP does not work with JSON-formatted results. The JSONP parameters passed as arguments to a script are defined by the server.
By setting the value of setJsonp to true, both `get` and `getList` will be performed using JSonp instead of the regular GET.
You will need to add the 'JSON_CALLBACK' string to your URLs (see [$http.jsonp](http://docs.angularjs.org/api/ng.$http#methods_jsonp)). You can use `setDefaultRequestParams` to accomplish this:
```javascript
RestangularProvider.setDefaultRequestParams('jsonp', {callback: 'JSON_CALLBACK'});
```
#### setDefaultRequestParams
You can set default Query parameters to be sent with every request and every method.
Additionally, if you want to configure request params per method, you can use `requestParams` configuration similar to `$http`. For example `RestangularProvider.requestParams.get = {single: true}`.
Supported method to configure are: remove, get, post, put, common (all)
````javascript
// set params for multiple methods at once
Restangular.setDefaultRequestParams(['remove', 'post'], {confirm: true});
// set only for get method
Restangular.setDefaultRequestParams('get', {limit: 10});
// or for all supported request methods
Restangular.setDefaultRequestParams({apikey: "secret key"});
````
#### setFullResponse
You can set fullResponse to true to get the whole response every time you do any request. The full response has the restangularized data in the `data` field, and also has the headers and config sent. By default, it's set to false. Please note that in order for Restangular to access custom HTTP headers, your server must respond having the `Access-Control-Expose-Headers:` set.
````javascript
// set params for multiple methods at once
Restangular.setFullResponse(true);
````
Or set it per service
````javascript
// Restangular service that uses setFullResponse
app.factory('RestFulResponse', function(Restangular) {
return Restangular.withConfig(function(RestangularConfigurer) {
RestangularConfigurer.setFullResponse(true);
});
});
// Let's use it in the controller
app.controller('MainCtrl', function(Restangular, RestFulResponse) {
// Uses full response configuration
RestFulResponse.all('users').getList().then(function(response) {
$scope.users = response.data;
console.log(response.headers);
});
});
````
#### setDefaultHeaders
You can set default Headers to be sent with every request. Send format: {header_name: header_value}
````javascript
// set default header "token"
RestangularProvider.setDefaultHeaders({token: "x-restangular"});
````
#### setRequestSuffix
If all of your requests require to send some suffix to work, you can set it here. For example, if you need to send the format like `/users/123.json` you can add that `.json` to the suffix using the `setRequestSuffix` method
#### setUseCannonicalId
You can set this to either `true` or `false`. By default it's false. If set to true, then the cannonical ID from the element will be used for URL creation (in DELETE, PUT, POST, etc.). What this means is that if you change the ID of the element and then you do a put, if you set this to true, it'll use the "old" ID which was received from the server. If set to false, it'll use the new ID assigned to the element.
#### setPlainByDefault
You can set this to `true` or `false`. By default it's false. If set to true, data retrieved will be returned with no embed methods from restangular.
#### setEncodeIds
You can set here if you want to URL Encode IDs or not. By default, it's true.
**[Back to top](#table-of-contents)**
### Accessing configuration
You can also access the configuration via `RestangularProvider` and `Restangular` via the `configuration` property if you don't want to use the setters. Check it out:
````js
Restangular.configuration.requestSuffix = '/';
````
**[Back to top](#table-of-contents)**
### How to configure them globally
You can configure this in either the `config` or the `run` method. If your configurations don't need any other services, then I'd recommend you do them in the `config`. If your configurations depend on other services, you can configure them in the `run` using `Restangular` instead of `RestangularProvider`
#### Configuring in the `config`
````javascript
app.config(function(RestangularProvider) {
RestangularProvider.setBaseUrl('/api/v1');
RestangularProvider.setExtraFields(['name']);
RestangularProvider.setResponseExtractor(function(response, operation) {
return response.data;
});
RestangularProvider.addElementTransformer('accounts', false, function(element) {
element.accountName = 'Changed';
return element;
});
RestangularProvider.setDefaultHttpFields({cache: true});
RestangularProvider.setMethodOverriders(["put", "patch"]);
// In this case we are mapping the id of each element to the _id field.
// We also change the Restangular route.
// The default value for parentResource remains the same.
RestangularProvider.setRestangularFields({
id: "_id",
route: "restangularRoute",
selfLink: "self.href"
});
RestangularProvider.setRequestSuffix('.json');
// Use Request interceptor
RestangularProvider.setRequestInterceptor(function(element, operation, route, url) {
delete element.name;
return element;
});
// ..or use the full request interceptor, setRequestInterceptor's more powerful brother!
RestangularProvider.setFullRequestInterceptor(function(element, operation, route, url, headers, params, httpConfig) {
delete element.name;
return {
element: element,
params: _.extend(params, {single: true}),
headers: headers,
httpConfig: httpConfig
};
});
});
````
#### Configuring in the `run`
````javascript
// Here I inject the service BaseUrlCalculator which I need
app.run(function(Restangular, BaseUrlCalculator) {
Restangular.setBaseUrl(BaseUrlCalculator.calculate());
});
````
**[Back to top](#table-of-contents)**
### How to create a Restangular service with a different configuration from the global one
Let's assume that for most requests you need some configuration (The global one), and for just a bunch of methods you need another configuration. In that case, you'll need to create another Restangular service with this particular configuration. This scoped configuration will inherit all defaults from the global one. Let's see how.
````javascript
// Global configuration
app.config(function(RestangularProvider) {
RestangularProvider.setBaseUrl('http://www.google.com');
RestangularProvider.setRequestSuffix('.json');
});
// Restangular service that uses Bing
app.factory('BingRestangular', function(Restangular) {
return Restangular.withConfig(function(RestangularConfigurer) {
RestangularConfigurer.setBaseUrl('http://www.bing.com');
});
});
// Let's use them from a controller
app.controller('MainCtrl', function(Restangular, BingRestangular) {
// GET to http://www.google.com/users.json
// Uses global configuration
Restangular.all('users').getList()
// GET to http://www.bing.com/users.json
// Uses Bing configuration which is based on Global one, therefore .json is added.
BingRestangular.all('users').getList()
});
````
**[Back to top](#table-of-contents)**
### Decoupled Restangular Service
There're some times where you want to use Restangular but you don't want to expose Restangular object anywhere.
For those cases, you can actually use the `service` feature of Restangular.
Let's see how it works:
````js
// Declare factory
module.factory('Users', function(Restangular) {
return Restangular.service('users');
});
// In your controller you inject Users
Users.get(2) // GET to /users/2
Users.one(2).get() // GET to /users/2
Users.post({data}) // POST to /users
// GET to /users
Users.getList().then(function(users) {
var user = users[0]; // user === {id: 1, name: "Tonto"}
user.name = "Gonto";
// PUT to /users/1
user.put();
})
````
You can also use ```withHttpConfig``` on objects created by ```Restangular.service```.
```js
var personService = Restangular.service('person');
var entity = personService.withHttpConfig({transformRequest: function(data) {
data.fullname = data.firstname + ' ' + data.lastname;
return JSON.stringify(data);
}}).post({
"lastname": "Mueller",
"firstname": "Gerd"
}).then(function(resp) {
console.log(resp);
});
```
We can also use Nested RESTful resources with this:
````js
var Cars = Restangular.service('cars', Restangular.one('users', 1));
Cars.getList() // GET to /users/1/cars
````
**[Back to top](#table-of-contents)**
## Methods description
There are 3 sets of methods. Collections have some methods and elements have others. There are are also some common methods for all of them
### Restangular methods
These are the methods that can be called on the Restangular object.
* **one(route, id)**: This will create a new Restangular object that is just a pointer to one element with the route `route` and the specified id.
* **all(route)**: This will create a new Restangular object that is just a pointer to a list of elements for the specified path.
* **oneUrl(route, url)**: This will create a new Restangular object that is just a pointer to one element with the specified URL.
* **allUrl(route, url)**: This creates a Restangular object that is just a pointer to a list at the specified URL.
* **copy(fromElement)**: This will create a copy of the from element so that we can modify the copied one.
* **restangularizeElement(parent, element, route, fromServer, collection, reqParams)**: Restangularizes a new element
* **restangularizeCollection(parent, element, route, fromServer, reqParams)**: Restangularizes a new collection
**[Back to top](#table-of-contents)**
### Element methods
* **get([queryParams, headers])**: Gets the element. Query params and headers are optionals
* **getList(subElement, [queryParams, headers])**: Gets a nested resource. subElement is mandatory. **It's a string with the name of the nested resource (and URL)**. For example `buildings`
* **put([queryParams, headers])**: Does a put to the current element
* **post(subElement, elementToPost, [queryParams, headers])**: Does a POST and creates a subElement. Subelement is mandatory and is the nested resource. Element to post is the object to post to the server
* **remove([queryParams, headers])**: Does a DELETE. By default, `remove` sends a request with an empty object, which may cause problems with some servers or browsers. [This](https://github.com/mgonto/restangular/issues/193) shows how to configure RESTangular to have no payload.
* **head([queryParams, headers])**: Does a HEAD
* **trace([queryParams, headers])**: Does a TRACE
* **options([queryParams, headers])**: Does a OPTIONS
* **patch(object, [queryParams, headers])**: Does a PATCH
* **one(route, id)**: Used for RequestLess connections and URL Building. See section below.
* **all(route)**: Used for RequestLess connections and URL Building. See section below.
* **several(route, ids*)**: Used for RequestLess connections and URL Building. See section below.
* **oneUrl(route, url)**: This will create a new Restangular object that is just a pointer to one element with the specified URL.
* **allUrl(route, url)**: This creates a Restangular object that is just a pointer to a list at the specified URL.
* **getRestangularUrl()**: Gets the URL of the current object.
* **getRequestedUrl()**: Gets the real URL the current object was requested with (incl. GET parameters). Will equal getRestangularUrl() when no parameters were used, before calling `get()`, or when using on a nested child.
* **getParentList()**: Gets the parent list to which it belongs (if any)
* **clone()**: Copies the element. It's an alias to calling `Restangular.copy(elem)`.
* **plain()**: Returns the plain element received from the server without any of the enhanced methods from Restangular. It's an alias to calling `Restangular.stripRestangular(elem)`
* **withHttpConfig(httpConfig)**: It lets you set a configuration for $http only for the next call. Check the Local Config HTTP section for an example.
* **save**: Calling save will determine whether to do PUT or POST accordingly
**[Back to top](#table-of-contents)**
### Collection methods
* **getList([queryParams, headers]): Gets itself again (Remember this is a collection)**.
* **get(id): Gets one item from the collection by id**.
* **post(elementToPost, [queryParams, headers])**: Creates a new element of this collection.
* **head([queryParams, headers])**: Does a HEAD
* **trace: ([queryParams, headers])**: Does a TRACE
* **options: ([queryParams, headers])**: Does a OPTIONS
* **patch(object, [queryParams, headers])**: Does a PATCH
* **remove([queryParams, headers])**: Does a DELETE. By default, `remove` sends a request with an empty object, which may cause problems with some servers or browsers. [This](https://github.com/mgonto/restangular/issues/193) shows how to configure RESTangular to have no payload.
* **putElement(idx, params, headers)**: Puts the element on the required index and returns a promise of the updated new array
* **getRestangularUrl()**: Gets the URL of the current object.
* **getRequestedUrl()**: Gets the real URL the current object was requested with (incl. GET parameters). Will equal getRestangularUrl() when no parameters were used, before calling `getList()`, or when using on a nested child.
* **one(route, id)**: Used for RequestLess connections and URL Building. See section below.
* **all(route)**: Used for RequestLess connections and URL Building. See section below.
* **several(route, ids*)**: Used for RequestLess connections and URL Building. See section below.
* **oneUrl(route, url)**: This will create a new Restangular object that is just a pointer to one element with the specified URL.
* **allUrl(route, url)**: This creates a Restangular object that is just a pointer to a list at the specified URL.
* **clone()**: Copies the collection. It's an alias to calling `Restangular.copy(collection)`.
* **withHttpConfig(httpConfig)**: It lets you set a configuration for $http only for the next call. Check the Local Config HTTP section for an example.
**[Back to top](#table-of-contents)**
### Custom methods
* **customGET(path, [params, headers])**: Does a GET to the specific path. Optionally you can set params and headers.
* **customGETLIST(path, [params, headers])**: Does a GET to the specific path. **In this case, you expect to get an array, not a single element**. Optionally you can set params and headers.
* **customDELETE(path, [params, headers])**: Does a DELETE to the specific path. Optionally you can set params and headers.
* **customPOST([elem, path, params, headers])**: Does a POST to the specific path. Optionally you can set params and headers and elem. Elem is the element to post. If it's not set, it's assumed that it's the element itself from which you're calling this function.
* **customPUT([elem, path, params, headers])**: Does a PUT to the specific path. Optionally you can set params and headers and elem. Elem is the element to post. If it's not set, it's assumed that it's the element itself from which you're calling this function.
* **customPATCH([elem, path, params, headers])**: Does a PATCH to the specific path. Accepts the same arguments as customPUT.
* **customOperation(operation, path, [params, headers, elem])**: This does a custom operation to the path that we specify. This method is actually used from all the others in this subsection. Operation can be one of: get, post, put, remove, head, options, patch, trace
* **addRestangularMethod(name, operation, [path, params, headers, elem])**: This will add a new restangular method to this object with the name `name` to the operation and path specified (or current path otherwise). There's a section on how to do this later.
Let's see an example of this:
````javascript
// GET /accounts/123/messages
Restangular.one("accounts", 123).customGET("messages")
// GET /accounts/messages?param=param2
Restangular.all("accounts").customGET("messages", {param: "param2"})
````
All custom methods have an alias where you replace `custom` by `do`. For example, `customGET` is equal to `doGET`. Just pick whatever syntax you prefer.
**[Back to top](#table-of-contents)**
## Copying elements
Before modifying an object, we sometimes want to copy it and then modify the copied object. We can't use `angular.copy` for this because it'll not change the `this` bound in the functions we add to the object. In this cases, you must use `Restangular.copy(fromElement)`.
**[Back to top](#table-of-contents)**
## Enhanced promises
Restangular uses enhanced promises when returning. What does this mean? All promises returned now have 2 additional methods and collection promises have 3. These are the methods:
* **call(methodName, params*)**: This will return a new promise of the previous value, after calling the method called methodName with the parameters params.
* **get(fieldName)**: This will return a new promise for the type of the field. The param of this new promise is the property `fieldName` from the original promise result.
* **push(object)**: This method will only be in the promises of arrays. It's a subset of the call method that does a push.
* **$object**: This returns the reference to the object that will be filled once the server responds a value. This means that if you call `getList` this will be an empty array by default. Once the array is returned from the server, this same `$object` property will get filled with results from the server.
I know these explanations are quite complicated, so let's see an example :D.
````javascript
var buildings = Restangular.all("buildings").getList();
// New promise after adding the new building
// Now you can show in scope this newBuildings promise and it'll show all the buildings
// received from server plus the new one added
var newBuildings = buildings.push({name: "gonto"});
var newBuildingsSame = buildings.call("push", {name: "gonto"});
// This is a promise of a number value. You can show it in the UI
var lengthPromise = buildings.get("length");
lengthPromise.then(function(length) {
// Here the length is the real length value of the returned collection of buildings
});
````
**[Back to top](#table-of-contents)**
## Using values directly in templates
Since Angular 1.2, Promise unwrapping in templates has been disabled by default and will be deprecated soon.
**This means that the following will cease to work**:
````js
$scope.accounts = Restangular.all('accounts').getList();
````
````html
| {{account.name}} |
````
**As this was a really handy way of working with Restangular, I've made a feature similar to $resource that will enable this behavior again**:
````js
$scope.accounts = Restangular.all('accounts').getList().$object;
````
````html
| {{account.name}} |
````
The `$object` property is a new property I've added to promises. By default, it'll be an empty array or object. Once the sever has responded with the real value, that object or array is filled with the correct response, therefore making the ng-repeat work :). Pretty neat :D
**[Back to top](#table-of-contents)**
## Using Self reference resources
A lot of REST APIs return the URL to self of the element that you're querying. You can use that with Restangular so that you don't have to create the URLs yourself, but use the ones provided by the server.
Let's say that when doing a GET to `/people` you get the following
````javascript
[{
name: "Martin",
lastName: "Gontovnikas"
self: {
link: 'http://www.example.com/people/gonto'
}
}, {
name: "John",
lastName: "Wayne"
self: {
link: 'http://www.example.com/people/jhonny'
}
}]
````
In this case, as you can see, the URL to each element can't be guessed so we need to use that to reference the element. Restangular supports both relative and absolute URLs :).
How do we do this with Restangular?
First, we need to configure the path for the link to self. For that, in the config we do:
````javascript
RestangularProvider.setRestangularFields({
selfLink: 'self.link'
});
````
Then, we can just use this :)
````javascript
// Instead of using all we could also use allUrl to set a URL
// Restangular.allUrl('people', 'http://www.example.com/people')
Restangular.all('people').getList().then(function(people) {
var gonto = people[0];
gonto.name = "Owned";
// This will do a PUT to http://www.example.com/people/gonto
// It uses the self linking property :D
gonto.put()
})
````
**[Back to top](#table-of-contents)**
## URL Building
Sometimes, we have a lot of nested entities (and their IDs), but we just want the last child. In those cases, doing a request for everything to get the last child is overkill. For those cases, I've added the possibility to create URLs using the same API as creating a new Restangular object. This connections are created without making any requests. Let's see how to do this:
````javascript
var restangularSpaces = Restangular.one("accounts",123).one("buildings", 456).all("spaces");
// This will do ONE get to /accounts/123/buildings/456/spaces
restangularSpaces.getList()
// This will do ONE get to /accounts/123/buildings/456/spaces/789
Restangular.one("accounts", 123).one("buildings", 456).one("spaces", 789).get()
// POST /accounts/123/buildings/456/spaces
Restangular.one("accounts", 123).one("buildings", 456).all("spaces").post({name: "New Space"});
// DELETE /accounts/123/buildings/456
Restangular.one("accounts", 123).one("buildings", 456).remove();
````
**[Back to top](#table-of-contents)**
## Using local $http configuration
There're sometimes when you want to set a specific configuration $http configuration just for one Restangular's call. For that, you can use `withHttpConfig`. You must call that method just before doing the HTTP request. Let's learn how to use it with the following example:
````js
Restangular.one('accounts', 123).withHttpConfig({timeout: 100}).getList('buildings');
$scope.account.withHttpConfig({timeout: 100}).put();
````
**[Back to top](#table-of-contents)**
## Creating new Restangular Methods
Let's assume that your API needs some custom methods to work. If that's the case, always calling customGET or customPOST for that method with all parameters is a pain in the ass. That's why every element has a `addRestangularMethod` method.
This can be used together with the hook `addElementTransformer` to do some neat stuff. Let's see an example to learn this:
````javascript
// In your app configuration (config method)
// It will transform all building elements, NOT collections
RestangularProvider.addElementTransformer('buildings', false, function(building) {
// This will add a method called evaluate that will do a get to path evaluate with NO default
// query params and with some default header
// signature is (name, operation, path, params, headers, elementToPost)
building.addRestangularMethod('evaluate', 'get', 'evaluate', undefined, {'myHeader': 'value'});
return building;
});
RestangularProvider.addElementTransformer('users', true, function(user) {
// This will add a method called login that will do a POST to the path login
// signature is (name, operation, path, params, headers, elementToPost)
user.addRestangularMethod('login', 'post', 'login');
return user;
});
// Then, later in your code you can do the following:
// GET to /buildings/123/evaluate?myParam=param with headers myHeader: value
// Signature for this "custom created" methods is (params, headers, elem) if it's a safe operation (GET, OPTIONS, etc.)
// If it's an unsafe operation (POST, PUT, etc.), signature is (elem, params, headers).
// If something is set to any of this variables, the default set in the method creation will be overridden
// If nothing is set, then the defaults are sent
Restangular.one('buildings', 123).evaluate({myParam: 'param'});
// GET to /buildings/123/evaluate?myParam=param with headers myHeader: specialHeaderCase
Restangular.one('buildings', 123).evaluate({myParam: 'param'}, {'myHeader': 'specialHeaderCase'});
// Here the body of the POST is going to be {key: value} as POST is an unsafe operation
Restangular.all('users').login({key: value});
````
**[Back to top](#table-of-contents)**
## Adding Custom Methods to Collections
Create custom methods for your collection using Restangular.extendCollection(). This is an alias for:
```js
Restangular.addElementTransformer(route, true, fn);
```
### Example:
```js
// create methods for your collection
Restangular.extendCollection('accounts', function(collection) {
collection.totalAmount = function() {
// implementation here
};
return collection;
});
var accountsPromise = Restangular.all('accounts').getList();
accountsPromise.then(function(accounts) {
accounts.totalAmount(); // invoke your custom collection method
});
```
**[Back to top](#table-of-contents)**
## Adding Custom Methods to Models
Create custom methods for your models using Restangular.extendModel(). This is an alias for:
```js
Restangular.addElementTransformer(route, false, fn);
```
**[Back to top](#table-of-contents)**
### Example:
```js
Restangular.extendModel('accounts', function(model) {
model.prettifyAmount = function() {};
return model;
});
var accountPromise = Restangular.one('accounts', 1).get();
accountPromise.then(function(account) {
account.prettifyAmount(); // invoke your custom model method
});
```
**[Back to top](#table-of-contents)**
# FAQ
#### **How can I handle errors?**
Errors can be checked on the second argument of the then.
````javascript
Restangular.all("accounts").getList().then(function() {
console.log("All ok");
}, function(response) {
console.log("Error with status code", response.status);
});
````
#### **I need to send one header in EVERY Restangular request, how do I do this?**
You can use `defaultHeaders` property for this or `$httpProvider.defaults.headers`, whichever suits you better. `defaultsHeaders` can be scoped with `withConfig` so it's really cool.
#### Can I cache requests?
`$http` can cache requests if you send the property `cache` to true. You can do that for every Restangular request by using `defaultHttpFields` property. This is the way:
````javascript
RestangularProvider.setDefaultHttpFields({cache: true});
````
#### Can it be used in `$routeProvider.resolve`?
Yes, of course. Every method in Restangular returns a promise so this can be used without any problem.
#### **How can I send a delete WITHOUT a body?**
You must add a requestInterceptor for this.
````js
RestangularProvider.setRequestInterceptor(function(elem, operation) {
if (operation === "remove") {
return null;
}
return elem;
})
````
#### **My response is actually wrapped with some metadata. How do I get the data in that case?**
So, let's assume that your data is the following:
````javascript
// When getting the list, this is the response.
{
"status":"success",
"data": {
"data": [{
"id":1,
// More data
}],
"meta": {
"totalRecord":100
}
}
}
// When getting a single element, this is the response.
{
"status":"success",
"data": {
"id" : 1
// More data
}
}
````
In this case, you'd need to use RestangularProvider's `addResponseInterceptor`. See the following:
````javascript
app.config(function(RestangularProvider) {
// add a response interceptor
RestangularProvider.addResponseInterceptor(function(data, operation, what, url, response, deferred) {
var extractedData;
// .. to look for getList operations
if (operation === "getList") {
// .. and handle the data and meta data
extractedData = data.data.data;
extractedData.meta = data.data.meta;
} else {
extractedData = data.data;
}
return extractedData;
});
});
````
#### **I use Mongo and the ID of the elements is `_id` not `id` as the default. Therefore requests are sent to undefined routes**
What you need to do is to configure the `RestangularFields` and set the `id` field to `_id`. Let's see how:
````javascript
RestangularProvider.setRestangularFields({
id: "_id"
});
````
#### **What if each of my models has a different ID name like CustomerID for Customer**
In some cases, people have different ID name for each entity. For example, they have CustomerID for customer and EquipmentID for Equipment. If that's the case, you can override Restangular's getIdFromElem. For that, you need to do:
````js
RestangularProvider.configuration.getIdFromElem = function(elem) {
// if route is customers ==> returns customerID
return elem[_.initial(elem.route).join('') + "ID"];
}
````
With that, you'd get what you need :)
#### **How can I send files in my request using Restangular?**
This can be done using the customPOST / customPUT method. Look at the following example:
````js
Restangular.all('users')
.withHttpConfig({transformRequest: angular.identity})
.customPOST(formData, undefined, undefined,
{ 'Content-Type': undefined });
````
This basically tells the request to use the *Content-Type: multipart/form-data* as the header. Also *formData* is the body of the request, be sure to add all the params here, including the File you want to send of course. There is an issue already closed but with a lot of information from other users and @mgonto as well: [GitHub - Restangular](https://github.com/mgonto/restangular/issues/420)
#### **How do I handle CRUD operations in a List returned by Restangular?**
The best option for doing CRUD operations with a list, is to actually use the "real" list, and not the promise. It makes it easy to interact with it.
Let's see an example :).
````javascript
// Here we use then to resolve the promise.
Restangular.all('users').getList().then(function(users) {
$scope.users = users;
var userWithId = _.find(users, function(user) {
return user.id === 123;
});
userWithId.name = "Gonto";
userWithId.put();
// Alternatively delete the element from the list when finished
userWithId.remove().then(function() {
// Updating the list and removing the user after the response is OK.
$scope.users = _.without($scope.users, userWithId);
});
});
````
When you actually get a list by doing
````javascript
$scope.owners = house.getList('owners').$object;
````
You're actually assigning a Promise to the owners value of the $scope. As Angular knows how to process promises, if in your view you do an ng-repeat of this $scope variable, results will be shown once the promise is resolved (Response arrived).
However, changes to that promise that you do from your HTML won't be seen in the scope, as it's not a real array. It's just a promise of an array.
#### Removing an element from a collection, keeping the collection restangularized
While the example above removes the deleted user from the collection, it also overwrites the collection object with a plain array (because of `_.without`) which no longer knows about its Restangular attributes.
If want to keep the restangularized collection, remove the element by modifying the collection in place:
```javascript
userWithId.remove().then(function() {
var index = $scope.users.indexOf(userWithId);
if (index > -1) $scope.users.splice(index, 1);
});
```
#### When I set baseUrl with a port, it's stripped out.
It won't be stripped out anymore as I've ditched `$resource` :). Now you can happily put the port :).
#### How can I access the `unrestangularized` element as well as the `restangularized` one?
In order to get this done, you need to use the `responseExtractor`. You need to set a property there that will point to the original response received. Also, you need to actually copy this response as that response is the one that's going to be `restangularized` later
````javascript
RestangularProvider.setResponseExtractor(function(response) {
var newResponse = response;
if (angular.isArray(response)) {
angular.forEach(newResponse, function(value, key) {
newResponse[key].originalElement = angular.copy(value);
});
} else {
newResponse.originalElement = angular.copy(response);
}
return newResponse;
});
````
Alternatively, if you just want the stripped out response on any given call, you can use the .plain() method, doing something like this:
````javascript
$scope.showData = function () {
baseUrl.post(someData).then(function(response) {
console.log(response.plain());
});
};
````
**Addendum :** If you want originalElement to be the original response object instead of having an original value for each key in your newResponse array, replace
````
newResponse[key].originalElement = angular.copy(value);
````
By
````
newResponse.originalElement[key] = angular.copy(value);
````
#### Restangular fails with status code 0
This is typically caused by Cross Origin Request policy. In order to enable cross domain communication and get correct response with appropriate status codes, you must have the CORS headers attached, even in error responses. If the server does not attach the CORS headers to the response then the XHR object won't parse it, thus the XHR object won't have any response body, status or any other response data inside which typically will cause your request to fail with status code 0.
#### Why does this depend on Lodash / Underscore?
This is a very good question. I could've done the code so that I don't depend on Underscore nor Lodash, but I think both libraries make your life SO much easier. They have all of the "functional" stuff like map, reduce, filter, find, etc.
With these libraries, you always work with immutable stuff, you get compatibility for browsers which don't implement ECMA5 nor some of these cool methods, and they're actually quicker.
So, why not use it? If you've never heard of them, by using Restangular, you could start using them. Trust me, you're never going to give them up after this!
**[Back to top](#table-of-contents)**
#### How do I cancel a request?
Sometimes you may wish to cancel a request, this is how you would do it:
```
var canceler = $q.defer();
Restangular.all('users').withHttpConfig({timeout: canceler.promise}).get();
canceler.resolve(); // cancels the request
```
This is a little counterintuitive, so let me explain. Restangular is built on top of `$http`, which takes a timeout parameter. As per the $http docs:
timeout in milliseconds, or promise that should abort the request when resolved.
Resolving the promise (canceler in this case), will cancel the request.
**[Back to top](#table-of-contents)**
# Supported Angular versions
Restangular supports all Angular versions from 1.0.X - 1.5.X
Also, when using Restangular with version >= 1.1.4, in case you're using Restangular inside a callback not handled by Angular, you have to wrap the whole request with `$scope.apply` to make it work or you need to run one extra `$digest` manually. Check out https://github.com/mgonto/restangular/issues/71
**[Back to top](#table-of-contents)**
# Server Frameworks
Users reported that this server frameworks play real nice with Restangular, as they let you create a Nested RESTful Resources API easily:
* Ruby on Rails
* CakePHP, Laravel and FatFREE, Symfony2 with RestBundle, Silex for PHP
* Play1 & 2 for Java & scala
* Dropwizard for Java
* Restify and Express for NodeJS
* Tastypie and Django Rest Framework for Django
* Slim Framework
* Symfony2 with FOSRestBundle (PHP)
* Microsoft ASP.NET Web API 2
* Grails Framework ([example](https://github.com/rmondejar/grails-angular-duo))
**[Back to top](#table-of-contents)**
# Releases Notes
New releases notes are together with releases in GitHub at: https://github.com/mgonto/restangular/releases
To see old releases notes, [you can click here](https://github.com/mgonto/restangular/blob/master/CHANGELOG.md)
**[Back to top](#table-of-contents)**
# Contributors
* Martin Gontovnikas ([@mgonto](https://twitter.com/mgonto))
* Paul Dijou ([@paul_dijou](https://twitter.com/paul_dijou))
**[Back to top](#table-of-contents)**
# License
The MIT License
Copyright (c) 2014 Martin Gontovnikas http://www.gon.to/
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.
[](https://bitdeli.com/free "Bitdeli Badge")
**[Back to top](#table-of-contents)**
================================================
FILE: bower.json
================================================
{
"name": "restangular",
"main": "./dist/restangular.js",
"description": "Restful Resources service for AngularJS apps",
"repository": {
"type": "git",
"url": "git://github.com/mgonto/restangular.git"
},
"dependencies": {
"lodash": "~4.17.0",
"angular": "~1.x"
},
"ignore": [
"node_modules",
"components",
"lib"
]
}
================================================
FILE: dist/restangular.js
================================================
/**
* Restful Resources service for AngularJS apps
* @version v1.6.1 - 2017-01-06 * @link https://github.com/mgonto/restangular
* @author Martin Gontovnikas
* @license MIT License, http://www.opensource.org/licenses/MIT
*/(function(root, factory) {
/* global define, require */
// https://github.com/umdjs/umd/blob/master/templates/returnExports.js
if (typeof define === 'function' && define.amd) {
define(['lodash', 'angular'], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(require('lodash'), require('angular'));
} else {
// No global export, Restangular will register itself as Angular.js module
factory(root._, root.angular);
}
}(this, function(_, angular) {
var restangular = angular.module('restangular', []);
restangular.provider('Restangular', function() {
// Configuration
var Configurer = {};
Configurer.init = function(object, config) {
object.configuration = config;
/**
* Those are HTTP safe methods for which there is no need to pass any data with the request.
*/
var safeMethods = ['get', 'head', 'options', 'trace', 'getlist'];
config.isSafe = function(operation) {
return _.includes(safeMethods, operation.toLowerCase());
};
var absolutePattern = /^https?:\/\//i;
config.isAbsoluteUrl = function(string) {
return _.isUndefined(config.absoluteUrl) || _.isNull(config.absoluteUrl) ?
string && absolutePattern.test(string) :
config.absoluteUrl;
};
config.absoluteUrl = _.isUndefined(config.absoluteUrl) ? true : config.absoluteUrl;
object.setSelfLinkAbsoluteUrl = function(value) {
config.absoluteUrl = value;
};
/**
* This is the BaseURL to be used with Restangular
*/
config.baseUrl = _.isUndefined(config.baseUrl) ? '' : config.baseUrl;
object.setBaseUrl = function(newBaseUrl) {
config.baseUrl = /\/$/.test(newBaseUrl) ?
newBaseUrl.substring(0, newBaseUrl.length - 1) :
newBaseUrl;
return this;
};
/**
* Sets the extra fields to keep from the parents
*/
config.extraFields = config.extraFields || [];
object.setExtraFields = function(newExtraFields) {
config.extraFields = newExtraFields;
return this;
};
/**
* Some default $http parameter to be used in EVERY call
**/
config.defaultHttpFields = config.defaultHttpFields || {};
object.setDefaultHttpFields = function(values) {
config.defaultHttpFields = values;
return this;
};
/**
* Always return plain data, no restangularized object
**/
config.plainByDefault = config.plainByDefault || false;
object.setPlainByDefault = function(value) {
config.plainByDefault = value === true ? true : false;
return this;
};
config.withHttpValues = function(httpLocalConfig, obj) {
return _.defaults(obj, httpLocalConfig, config.defaultHttpFields);
};
config.encodeIds = _.isUndefined(config.encodeIds) ? true : config.encodeIds;
object.setEncodeIds = function(encode) {
config.encodeIds = encode;
};
config.defaultRequestParams = config.defaultRequestParams || {
get: {},
post: {},
put: {},
remove: {},
common: {}
};
object.setDefaultRequestParams = function(param1, param2) {
var methods = [],
params = param2 || param1;
if (!_.isUndefined(param2)) {
if (_.isArray(param1)) {
methods = param1;
} else {
methods.push(param1);
}
} else {
methods.push('common');
}
_.each(methods, function(method) {
config.defaultRequestParams[method] = params;
});
return this;
};
object.requestParams = config.defaultRequestParams;
config.defaultHeaders = config.defaultHeaders || {};
object.setDefaultHeaders = function(headers) {
config.defaultHeaders = headers;
object.defaultHeaders = config.defaultHeaders;
return this;
};
object.defaultHeaders = config.defaultHeaders;
/**
* Method overriders will set which methods are sent via POST with an X-HTTP-Method-Override
**/
config.methodOverriders = config.methodOverriders || [];
object.setMethodOverriders = function(values) {
var overriders = _.extend([], values);
if (config.isOverridenMethod('delete', overriders)) {
overriders.push('remove');
}
config.methodOverriders = overriders;
return this;
};
config.jsonp = _.isUndefined(config.jsonp) ? false : config.jsonp;
object.setJsonp = function(active) {
config.jsonp = active;
};
config.isOverridenMethod = function(method, values) {
var search = values || config.methodOverriders;
return !_.isUndefined(_.find(search, function(one) {
return one.toLowerCase() === method.toLowerCase();
}));
};
/**
* Sets the URL creator type. For now, only Path is created. In the future we'll have queryParams
**/
config.urlCreator = config.urlCreator || 'path';
object.setUrlCreator = function(name) {
if (!_.has(config.urlCreatorFactory, name)) {
throw new Error('URL Path selected isn\'t valid');
}
config.urlCreator = name;
return this;
};
/**
* You can set the restangular fields here. The 3 required fields for Restangular are:
*
* id: Id of the element
* route: name of the route of this element
* parentResource: the reference to the parent resource
*
* All of this fields except for id, are handled (and created) by Restangular. By default,
* the field values will be id, route and parentResource respectively
*/
config.restangularFields = config.restangularFields || {
id: 'id',
route: 'route',
parentResource: 'parentResource',
restangularCollection: 'restangularCollection',
cannonicalId: '__cannonicalId',
etag: 'restangularEtag',
selfLink: 'href',
get: 'get',
getList: 'getList',
put: 'put',
post: 'post',
remove: 'remove',
head: 'head',
trace: 'trace',
options: 'options',
patch: 'patch',
getRestangularUrl: 'getRestangularUrl',
getRequestedUrl: 'getRequestedUrl',
putElement: 'putElement',
addRestangularMethod: 'addRestangularMethod',
getParentList: 'getParentList',
clone: 'clone',
ids: 'ids',
httpConfig: '_$httpConfig',
reqParams: 'reqParams',
one: 'one',
all: 'all',
several: 'several',
oneUrl: 'oneUrl',
allUrl: 'allUrl',
customPUT: 'customPUT',
customPATCH: 'customPATCH',
customPOST: 'customPOST',
customDELETE: 'customDELETE',
customGET: 'customGET',
customGETLIST: 'customGETLIST',
customOperation: 'customOperation',
doPUT: 'doPUT',
doPATCH: 'doPATCH',
doPOST: 'doPOST',
doDELETE: 'doDELETE',
doGET: 'doGET',
doGETLIST: 'doGETLIST',
fromServer: 'fromServer',
withConfig: 'withConfig',
withHttpConfig: 'withHttpConfig',
singleOne: 'singleOne',
plain: 'plain',
save: 'save',
restangularized: 'restangularized'
};
object.setRestangularFields = function(resFields) {
config.restangularFields =
_.extend(config.restangularFields, resFields);
return this;
};
config.isRestangularized = function(obj) {
return !!obj[config.restangularFields.restangularized];
};
config.setFieldToElem = function(field, elem, value) {
var properties = field.split('.');
var idValue = elem;
_.each(_.initial(properties), function(prop) {
idValue[prop] = {};
idValue = idValue[prop];
});
idValue[_.last(properties)] = value;
return this;
};
config.getFieldFromElem = function(field, elem) {
var properties = field.split('.');
var idValue = elem;
_.each(properties, function(prop) {
if (idValue) {
idValue = idValue[prop];
}
});
return angular.copy(idValue);
};
config.setIdToElem = function(elem, id /*, route */ ) {
config.setFieldToElem(config.restangularFields.id, elem, id);
return this;
};
config.getIdFromElem = function(elem) {
return config.getFieldFromElem(config.restangularFields.id, elem);
};
config.isValidId = function(elemId) {
return '' !== elemId && !_.isUndefined(elemId) && !_.isNull(elemId);
};
config.setUrlToElem = function(elem, url /*, route */ ) {
config.setFieldToElem(config.restangularFields.selfLink, elem, url);
return this;
};
config.getUrlFromElem = function(elem) {
return config.getFieldFromElem(config.restangularFields.selfLink, elem);
};
config.useCannonicalId = _.isUndefined(config.useCannonicalId) ? false : config.useCannonicalId;
object.setUseCannonicalId = function(value) {
config.useCannonicalId = value;
return this;
};
config.getCannonicalIdFromElem = function(elem) {
var cannonicalId = elem[config.restangularFields.cannonicalId];
var actualId = config.isValidId(cannonicalId) ? cannonicalId : config.getIdFromElem(elem);
return actualId;
};
/**
* Sets the Response parser. This is used in case your response isn't directly the data.
* For example if you have a response like {meta: {'meta'}, data: {name: 'Gonto'}}
* you can extract this data which is the one that needs wrapping
*
* The ResponseExtractor is a function that receives the response and the method executed.
*/
config.responseInterceptors = config.responseInterceptors || [];
config.defaultResponseInterceptor = function(data /*, operation, what, url, response, deferred */ ) {
return data;
};
config.responseExtractor = function(data, operation, what, url, response, deferred) {
var interceptors = angular.copy(config.responseInterceptors);
interceptors.push(config.defaultResponseInterceptor);
var theData = data;
_.each(interceptors, function(interceptor) {
theData = interceptor(theData, operation,
what, url, response, deferred);
});
return theData;
};
object.addResponseInterceptor = function(extractor) {
config.responseInterceptors.push(extractor);
return this;
};
config.errorInterceptors = config.errorInterceptors || [];
object.addErrorInterceptor = function(interceptor) {
config.errorInterceptors.push(interceptor);
return this;
};
object.setResponseInterceptor = object.addResponseInterceptor;
object.setResponseExtractor = object.addResponseInterceptor;
object.setErrorInterceptor = object.addErrorInterceptor;
/**
* Response interceptor is called just before resolving promises.
*/
/**
* Request interceptor is called before sending an object to the server.
*/
config.requestInterceptors = config.requestInterceptors || [];
config.defaultInterceptor = function(element, operation, path, url, headers, params, httpConfig) {
return {
element: element,
headers: headers,
params: params,
httpConfig: httpConfig
};
};
config.fullRequestInterceptor = function(element, operation, path, url, headers, params, httpConfig) {
var interceptors = angular.copy(config.requestInterceptors);
var defaultRequest = config.defaultInterceptor(element, operation, path, url, headers, params, httpConfig);
return _.reduce(interceptors, function(request, interceptor) {
return _.extend(request, interceptor(request.element, operation,
path, url, request.headers, request.params, request.httpConfig));
}, defaultRequest);
};
object.addRequestInterceptor = function(interceptor) {
config.requestInterceptors.push(function(elem, operation, path, url, headers, params, httpConfig) {
return {
headers: headers,
params: params,
element: interceptor(elem, operation, path, url),
httpConfig: httpConfig
};
});
return this;
};
object.setRequestInterceptor = object.addRequestInterceptor;
object.addFullRequestInterceptor = function(interceptor) {
config.requestInterceptors.push(interceptor);
return this;
};
object.setFullRequestInterceptor = object.addFullRequestInterceptor;
config.onBeforeElemRestangularized = config.onBeforeElemRestangularized || function(elem) {
return elem;
};
object.setOnBeforeElemRestangularized = function(post) {
config.onBeforeElemRestangularized = post;
return this;
};
object.setRestangularizePromiseInterceptor = function(interceptor) {
config.restangularizePromiseInterceptor = interceptor;
return this;
};
/**
* This method is called after an element has been "Restangularized".
*
* It receives the element, a boolean indicating if it's an element or a collection
* and the name of the model
*
*/
config.onElemRestangularized = config.onElemRestangularized || function(elem) {
return elem;
};
object.setOnElemRestangularized = function(post) {
config.onElemRestangularized = post;
return this;
};
config.shouldSaveParent = config.shouldSaveParent || function() {
return true;
};
object.setParentless = function(values) {
if (_.isArray(values)) {
config.shouldSaveParent = function(route) {
return !_.includes(values, route);
};
} else if (_.isBoolean(values)) {
config.shouldSaveParent = function() {
return !values;
};
}
return this;
};
/**
* This lets you set a suffix to every request.
*
* For example, if your api requires that for JSon requests you do /users/123.json, you can set that
* in here.
*
*
* By default, the suffix is null
*/
config.suffix = _.isUndefined(config.suffix) ? null : config.suffix;
object.setRequestSuffix = function(newSuffix) {
config.suffix = newSuffix;
return this;
};
/**
* Add element transformers for certain routes.
*/
config.transformers = config.transformers || {};
config.matchTransformers = config.matchTransformers || [];
object.addElementTransformer = function(type, secondArg, thirdArg) {
var isCollection = null;
var transformer = null;
if (arguments.length === 2) {
transformer = secondArg;
} else {
transformer = thirdArg;
isCollection = secondArg;
}
var transformerFn = function(coll, elem) {
if (_.isNull(isCollection) || (coll === isCollection)) {
return transformer(elem);
}
return elem;
};
if (_.isRegExp(type)) {
config.matchTransformers.push({
regexp: type,
transformer: transformerFn
});
} else {
if (!config.transformers[type]) {
config.transformers[type] = [];
}
config.transformers[type].push(transformerFn);
}
return object;
};
object.extendCollection = function(route, fn) {
return object.addElementTransformer(route, true, fn);
};
object.extendModel = function(route, fn) {
return object.addElementTransformer(route, false, fn);
};
config.transformElem = function(elem, isCollection, route, Restangular, force) {
if (!force && !config.transformLocalElements && !elem[config.restangularFields.fromServer]) {
return elem;
}
var changedElem = elem;
var matchTransformers = config.matchTransformers;
if (matchTransformers) {
_.each(matchTransformers, function(transformer) {
if (transformer.regexp.test(route)) {
changedElem = transformer.transformer(isCollection, changedElem);
}
});
}
var typeTransformers = config.transformers[route];
if (typeTransformers) {
_.each(typeTransformers, function(transformer) {
changedElem = transformer(isCollection, changedElem);
});
}
return config.onElemRestangularized(changedElem, isCollection, route, Restangular);
};
config.transformLocalElements = _.isUndefined(config.transformLocalElements) ?
false :
config.transformLocalElements;
object.setTransformOnlyServerElements = function(active) {
config.transformLocalElements = !active;
};
config.fullResponse = _.isUndefined(config.fullResponse) ? false : config.fullResponse;
object.setFullResponse = function(full) {
config.fullResponse = full;
return this;
};
//Internal values and functions
config.urlCreatorFactory = {};
/**
* Base URL Creator. Base prototype for everything related to it
**/
var BaseCreator = function() {};
BaseCreator.prototype.setConfig = function(config) {
this.config = config;
return this;
};
BaseCreator.prototype.parentsArray = function(current) {
var parents = [];
while (current) {
parents.push(current);
current = current[this.config.restangularFields.parentResource];
}
return parents.reverse();
};
function RestangularResource(config, $http, url, configurer) {
var resource = {};
_.each(_.keys(configurer), function(key) {
var value = configurer[key];
// Add default parameters
value.params = _.extend({}, value.params, config.defaultRequestParams[value.method.toLowerCase()]);
// We don't want the ? if no params are there
if (_.isEmpty(value.params)) {
delete value.params;
}
if (config.isSafe(value.method)) {
resource[key] = function() {
return $http(_.extend(value, {
url: url
}));
};
} else {
resource[key] = function(data) {
return $http(_.extend(value, {
url: url,
data: data
}));
};
}
});
return resource;
}
BaseCreator.prototype.resource = function(current, $http, localHttpConfig, callHeaders, callParams, what, etag, operation) {
var params = _.defaults(callParams || {}, this.config.defaultRequestParams.common);
var headers = _.defaults(callHeaders || {}, this.config.defaultHeaders);
if (etag) {
if (!config.isSafe(operation)) {
headers['If-Match'] = etag;
} else {
headers['If-None-Match'] = etag;
}
}
var url = this.base(current);
if (what || what === 0) {
var add = '';
if (!/\/$/.test(url)) {
add += '/';
}
add += what;
url += add;
}
if (this.config.suffix &&
url.indexOf(this.config.suffix, url.length - this.config.suffix.length) === -1 &&
!this.config.getUrlFromElem(current)) {
url += this.config.suffix;
}
current[this.config.restangularFields.httpConfig] = undefined;
return RestangularResource(this.config, $http, url, {
getList: this.config.withHttpValues(localHttpConfig, {
method: 'GET',
params: params,
headers: headers
}),
get: this.config.withHttpValues(localHttpConfig, {
method: 'GET',
params: params,
headers: headers
}),
jsonp: this.config.withHttpValues(localHttpConfig, {
method: 'jsonp',
params: params,
headers: headers
}),
put: this.config.withHttpValues(localHttpConfig, {
method: 'PUT',
params: params,
headers: headers
}),
post: this.config.withHttpValues(localHttpConfig, {
method: 'POST',
params: params,
headers: headers
}),
remove: this.config.withHttpValues(localHttpConfig, {
method: 'DELETE',
params: params,
headers: headers
}),
head: this.config.withHttpValues(localHttpConfig, {
method: 'HEAD',
params: params,
headers: headers
}),
trace: this.config.withHttpValues(localHttpConfig, {
method: 'TRACE',
params: params,
headers: headers
}),
options: this.config.withHttpValues(localHttpConfig, {
method: 'OPTIONS',
params: params,
headers: headers
}),
patch: this.config.withHttpValues(localHttpConfig, {
method: 'PATCH',
params: params,
headers: headers
})
});
};
/**
* This is the Path URL creator. It uses Path to show Hierarchy in the Rest API.
* This means that if you have an Account that then has a set of Buildings, a URL to a building
* would be /accounts/123/buildings/456
**/
var Path = function() {};
Path.prototype = new BaseCreator();
Path.prototype.normalizeUrl = function(url) {
var parts = /((?:http[s]?:)?\/\/)?(.*)?/.exec(url);
parts[2] = parts[2].replace(/[\\\/]+/g, '/');
return (typeof parts[1] !== 'undefined') ? parts[1] + parts[2] : parts[2];
};
Path.prototype.base = function(current) {
var __this = this;
return _.reduce(this.parentsArray(current), function(acum, elem) {
var elemUrl;
var elemSelfLink = __this.config.getUrlFromElem(elem);
if (elemSelfLink) {
if (__this.config.isAbsoluteUrl(elemSelfLink)) {
return elemSelfLink;
} else {
elemUrl = elemSelfLink;
}
} else {
elemUrl = elem[__this.config.restangularFields.route];
if (elem[__this.config.restangularFields.restangularCollection]) {
var ids = elem[__this.config.restangularFields.ids];
if (ids) {
elemUrl += '/' + ids.join(',');
}
} else {
var elemId;
if (__this.config.useCannonicalId) {
elemId = __this.config.getCannonicalIdFromElem(elem);
} else {
elemId = __this.config.getIdFromElem(elem);
}
if (config.isValidId(elemId) && !elem.singleOne) {
elemUrl += '/' + (__this.config.encodeIds ? encodeURIComponent(elemId) : elemId);
}
}
}
acum = acum.replace(/\/$/, '') + '/' + elemUrl;
return __this.normalizeUrl(acum);
}, this.config.baseUrl);
};
Path.prototype.fetchUrl = function(current, what) {
var baseUrl = this.base(current);
if (what) {
baseUrl += '/' + what;
}
return baseUrl;
};
Path.prototype.fetchRequestedUrl = function(current, what) {
var url = this.fetchUrl(current, what);
var params = current[config.restangularFields.reqParams];
// From here on and until the end of fetchRequestedUrl,
// the code has been kindly borrowed from angular.js
// The reason for such code bloating is coherence:
// If the user were to use this for cache management, the
// serialization of parameters would need to be identical
// to the one done by angular for cache keys to match.
function sortedKeys(obj) {
var keys = [];
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
keys.push(key);
}
}
return keys.sort();
}
function forEachSorted(obj, iterator, context) {
var keys = sortedKeys(obj);
for (var i = 0; i < keys.length; i++) {
iterator.call(context, obj[keys[i]], keys[i]);
}
return keys;
}
function encodeUriQuery(val, pctEncodeSpaces) {
return encodeURIComponent(val).
replace(/%40/gi, '@').
replace(/%3A/gi, ':').
replace(/%24/g, '$').
replace(/%2C/gi, ',').
replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
}
if (!params) {
return url + (this.config.suffix || '');
}
var parts = [];
forEachSorted(params, function(value, key) {
if (value === null || value === undefined) {
return;
}
if (!angular.isArray(value)) {
value = [value];
}
angular.forEach(value, function(v) {
if (angular.isObject(v)) {
v = angular.toJson(v);
}
parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(v));
});
});
return url + (this.config.suffix || '') + ((url.indexOf('?') === -1) ? '?' : '&') + parts.join('&');
};
config.urlCreatorFactory.path = Path;
};
var globalConfiguration = {};
Configurer.init(this, globalConfiguration);
this.$get = ['$http', '$q', function($http, $q) {
function createServiceForConfiguration(config) {
var service = {};
var urlHandler = new config.urlCreatorFactory[config.urlCreator]();
urlHandler.setConfig(config);
function restangularizeBase(parent, elem, route, reqParams, fromServer) {
elem[config.restangularFields.route] = route;
elem[config.restangularFields.getRestangularUrl] = _.bind(urlHandler.fetchUrl, urlHandler, elem);
elem[config.restangularFields.getRequestedUrl] = _.bind(urlHandler.fetchRequestedUrl, urlHandler, elem);
elem[config.restangularFields.addRestangularMethod] = _.bind(addRestangularMethodFunction, elem);
elem[config.restangularFields.clone] = _.bind(copyRestangularizedElement, elem, elem);
elem[config.restangularFields.reqParams] = _.isEmpty(reqParams) ? null : reqParams;
elem[config.restangularFields.withHttpConfig] = _.bind(withHttpConfig, elem);
elem[config.restangularFields.plain] = _.bind(stripRestangular, elem, elem);
// Tag element as restangularized
elem[config.restangularFields.restangularized] = true;
// RequestLess connection
elem[config.restangularFields.one] = _.bind(one, elem, elem);
elem[config.restangularFields.all] = _.bind(all, elem, elem);
elem[config.restangularFields.several] = _.bind(several, elem, elem);
elem[config.restangularFields.oneUrl] = _.bind(oneUrl, elem, elem);
elem[config.restangularFields.allUrl] = _.bind(allUrl, elem, elem);
elem[config.restangularFields.fromServer] = !!fromServer;
if (parent && config.shouldSaveParent(route)) {
var parentId = config.getIdFromElem(parent);
var parentUrl = config.getUrlFromElem(parent);
var restangularFieldsForParent = _.union(
_.values(_.pick(config.restangularFields, ['route', 'singleOne', 'parentResource'])),
config.extraFields
);
var parentResource = _.pick(parent, restangularFieldsForParent);
if (config.isValidId(parentId)) {
config.setIdToElem(parentResource, parentId, route);
}
if (config.isValidId(parentUrl)) {
config.setUrlToElem(parentResource, parentUrl, route);
}
elem[config.restangularFields.parentResource] = parentResource;
} else {
elem[config.restangularFields.parentResource] = null;
}
return elem;
}
function one(parent, route, id, singleOne) {
var error;
if (_.isNumber(route) || _.isNumber(parent)) {
error = 'You\'re creating a Restangular entity with the number ';
error += 'instead of the route or the parent. For example, you can\'t call .one(12).';
throw new Error(error);
}
if (_.isUndefined(route)) {
error = 'You\'re creating a Restangular entity either without the path. ';
error += 'For example you can\'t call .one(). Please check if your arguments are valid.';
throw new Error(error);
}
var elem = {};
config.setIdToElem(elem, id, route);
config.setFieldToElem(config.restangularFields.singleOne, elem, singleOne);
return restangularizeElem(parent, elem, route, false);
}
function all(parent, route) {
return restangularizeCollection(parent, [], route, false);
}
function several(parent, route /*, ids */ ) {
var collection = [];
collection[config.restangularFields.ids] = Array.prototype.splice.call(arguments, 2);
return restangularizeCollection(parent, collection, route, false);
}
function oneUrl(parent, route, url) {
if (!route) {
throw new Error('Route is mandatory when creating new Restangular objects.');
}
var elem = {};
config.setUrlToElem(elem, url, route);
return restangularizeElem(parent, elem, route, false);
}
function allUrl(parent, route, url) {
if (!route) {
throw new Error('Route is mandatory when creating new Restangular objects.');
}
var elem = {};
config.setUrlToElem(elem, url, route);
return restangularizeCollection(parent, elem, route, false);
}
// Promises
function restangularizePromise(promise, isCollection, valueToFill) {
promise.call = _.bind(promiseCall, promise);
promise.get = _.bind(promiseGet, promise);
promise[config.restangularFields.restangularCollection] = isCollection;
if (isCollection) {
promise.push = _.bind(promiseCall, promise, 'push');
}
promise.$object = valueToFill;
if (config.restangularizePromiseInterceptor) {
config.restangularizePromiseInterceptor(promise);
}
return promise;
}
function promiseCall(method) {
var deferred = $q.defer();
var callArgs = arguments;
var filledValue = {};
this.then(function(val) {
var params = Array.prototype.slice.call(callArgs, 1);
var func = val[method];
func.apply(val, params);
filledValue = val;
deferred.resolve(val);
});
return restangularizePromise(deferred.promise, this[config.restangularFields.restangularCollection], filledValue);
}
function promiseGet(what) {
var deferred = $q.defer();
var filledValue = {};
this.then(function(val) {
filledValue = val[what];
deferred.resolve(filledValue);
});
return restangularizePromise(deferred.promise, this[config.restangularFields.restangularCollection], filledValue);
}
function resolvePromise(deferred, response, data, filledValue) {
_.extend(filledValue, data);
// Trigger the full response interceptor.
if (config.fullResponse) {
return deferred.resolve(_.extend(response, {
data: data
}));
} else {
deferred.resolve(data);
}
}
// Elements
function stripRestangular(elem) {
if (_.isArray(elem)) {
var array = [];
_.each(elem, function(value) {
array.push(config.isRestangularized(value) ? stripRestangular(value) : value);
});
return array;
} else {
return _.omit(elem, _.values(_.omit(config.restangularFields, 'id')));
}
}
function addCustomOperation(elem) {
elem[config.restangularFields.customOperation] = _.bind(customFunction, elem);
var requestMethods = {
get: customFunction,
delete: customFunction
};
_.each(['put', 'patch', 'post'], function(name) {
requestMethods[name] = function(operation, elem, path, params, headers) {
return _.bind(customFunction, this)(operation, path, params, headers, elem);
};
});
_.each(requestMethods, function(requestFunc, name) {
var callOperation = name === 'delete' ? 'remove' : name;
_.each(['do', 'custom'], function(alias) {
elem[alias + name.toUpperCase()] = _.bind(requestFunc, elem, callOperation);
});
});
elem[config.restangularFields.customGETLIST] = _.bind(fetchFunction, elem);
elem[config.restangularFields.doGETLIST] = elem[config.restangularFields.customGETLIST];
}
function copyRestangularizedElement(element) {
var copiedElement = angular.copy(element);
// check if we're dealing with a collection (i.e. an array)
// and restangularize the element using the proper restangularizer,
// element / collection
if (_.isArray(element)) {
return restangularizeCollection(
element[config.restangularFields.parentResource],
copiedElement,
element[config.restangularFields.route],
element[config.restangularFields.fromServer],
element[config.restangularFields.reqParams]
);
}
// not a collection, restangularize it as an element
return restangularizeElem(
element[config.restangularFields.parentResource],
copiedElement,
element[config.restangularFields.route],
element[config.restangularFields.fromServer],
element[config.restangularFields.restangularCollection],
element[config.restangularFields.reqParams]
);
}
function restangularizeElem(parent, element, route, fromServer, collection, reqParams) {
var elem = config.onBeforeElemRestangularized(element, false, route);
var localElem = restangularizeBase(parent, elem, route, reqParams, fromServer);
if (config.useCannonicalId) {
localElem[config.restangularFields.cannonicalId] = config.getIdFromElem(localElem);
}
if (collection) {
localElem[config.restangularFields.getParentList] = function() {
return collection;
};
}
localElem[config.restangularFields.restangularCollection] = false;
localElem[config.restangularFields.get] = _.bind(getFunction, localElem);
localElem[config.restangularFields.getList] = _.bind(fetchFunction, localElem);
localElem[config.restangularFields.put] = _.bind(putFunction, localElem);
localElem[config.restangularFields.post] = _.bind(postFunction, localElem);
localElem[config.restangularFields.remove] = _.bind(deleteFunction, localElem);
localElem[config.restangularFields.head] = _.bind(headFunction, localElem);
localElem[config.restangularFields.trace] = _.bind(traceFunction, localElem);
localElem[config.restangularFields.options] = _.bind(optionsFunction, localElem);
localElem[config.restangularFields.patch] = _.bind(patchFunction, localElem);
localElem[config.restangularFields.save] = _.bind(save, localElem);
addCustomOperation(localElem);
return config.transformElem(localElem, false, route, service, true);
}
function restangularizeCollection(parent, element, route, fromServer, reqParams) {
var elem = config.onBeforeElemRestangularized(element, true, route);
var localElem = restangularizeBase(parent, elem, route, reqParams, fromServer);
localElem[config.restangularFields.restangularCollection] = true;
localElem[config.restangularFields.post] = _.bind(postFunction, localElem, null);
localElem[config.restangularFields.remove] = _.bind(deleteFunction, localElem);
localElem[config.restangularFields.head] = _.bind(headFunction, localElem);
localElem[config.restangularFields.trace] = _.bind(traceFunction, localElem);
localElem[config.restangularFields.putElement] = _.bind(putElementFunction, localElem);
localElem[config.restangularFields.options] = _.bind(optionsFunction, localElem);
localElem[config.restangularFields.patch] = _.bind(patchFunction, localElem);
localElem[config.restangularFields.get] = _.bind(getById, localElem);
localElem[config.restangularFields.getList] = _.bind(fetchFunction, localElem, null);
addCustomOperation(localElem);
return config.transformElem(localElem, true, route, service, true);
}
function restangularizeCollectionAndElements(parent, element, route, fromServer) {
var collection = restangularizeCollection(parent, element, route, fromServer);
_.each(collection, function(elem) {
if (elem) {
restangularizeElem(parent, elem, route, fromServer);
}
});
return collection;
}
function getById(id, reqParams, headers) {
return this.customGET(id.toString(), reqParams, headers);
}
function putElementFunction(idx, params, headers) {
var __this = this;
var elemToPut = this[idx];
var deferred = $q.defer();
var filledArray = [];
filledArray = config.transformElem(filledArray, true, elemToPut[config.restangularFields.route], service);
elemToPut.put(params, headers).then(function(serverElem) {
var newArray = copyRestangularizedElement(__this);
newArray[idx] = serverElem;
filledArray = newArray;
deferred.resolve(newArray);
}, function(response) {
deferred.reject(response);
});
return restangularizePromise(deferred.promise, true, filledArray);
}
function parseResponse(resData, operation, route, fetchUrl, response, deferred) {
var data = config.responseExtractor(resData, operation, route, fetchUrl, response, deferred);
var etag = response.headers('ETag');
if (data && etag) {
data[config.restangularFields.etag] = etag;
}
return data;
}
function fetchFunction(what, reqParams, headers) {
var __this = this;
var deferred = $q.defer();
var operation = 'getList';
var url = urlHandler.fetchUrl(this, what);
var whatFetched = what || __this[config.restangularFields.route];
var request = config.fullRequestInterceptor(null, operation,
whatFetched, url, headers || {}, reqParams || {}, this[config.restangularFields.httpConfig] || {});
var filledArray = [];
filledArray = config.transformElem(filledArray, true, whatFetched, service);
var method = 'getList';
if (config.jsonp) {
method = 'jsonp';
}
var okCallback = function(response) {
var resData = response.data;
var fullParams = response.config.params;
var data = parseResponse(resData, operation, whatFetched, url, response, deferred);
// support empty response for getList() calls (some APIs respond with 204 and empty body)
if (_.isUndefined(data) || '' === data) {
data = [];
}
if (!_.isArray(data)) {
throw new Error('Response for getList SHOULD be an array and not an object or something else');
}
if (true === config.plainByDefault) {
return resolvePromise(deferred, response, data, filledArray);
}
var processedData = _.map(data, function(elem) {
if (!__this[config.restangularFields.restangularCollection]) {
return restangularizeElem(__this, elem, what, true, data);
} else {
return restangularizeElem(__this[config.restangularFields.parentResource],
elem, __this[config.restangularFields.route], true, data);
}
});
processedData = _.extend(data, processedData);
if (!__this[config.restangularFields.restangularCollection]) {
resolvePromise(
deferred,
response,
restangularizeCollection(
__this,
processedData,
what,
true,
fullParams
),
filledArray
);
} else {
resolvePromise(
deferred,
response,
restangularizeCollection(
__this[config.restangularFields.parentResource],
processedData,
__this[config.restangularFields.route],
true,
fullParams
),
filledArray
);
}
};
urlHandler.resource(this, $http, request.httpConfig, request.headers, request.params, what,
this[config.restangularFields.etag], operation)[method]().then(okCallback, function error(response) {
if (response.status === 304 && __this[config.restangularFields.restangularCollection]) {
resolvePromise(deferred, response, __this, filledArray);
} else if (_.every(config.errorInterceptors, function(cb) {
return cb(response, deferred, okCallback) !== false;
})) {
// triggered if no callback returns false
deferred.reject(response);
}
});
return restangularizePromise(deferred.promise, true, filledArray);
}
function withHttpConfig(httpConfig) {
this[config.restangularFields.httpConfig] = httpConfig;
return this;
}
function save(params, headers) {
if (this[config.restangularFields.fromServer]) {
return this[config.restangularFields.put](params, headers);
} else {
return _.bind(elemFunction, this)('post', undefined, params, undefined, headers);
}
}
function elemFunction(operation, what, params, obj, headers) {
var __this = this;
var deferred = $q.defer();
var resParams = params || {};
var route = what || this[config.restangularFields.route];
var fetchUrl = urlHandler.fetchUrl(this, what);
var callObj = obj || this;
// fallback to etag on restangular object (since for custom methods we probably don't explicitly specify the etag field)
var etag = callObj[config.restangularFields.etag] || (operation !== 'post' ? this[config.restangularFields.etag] : null);
if (_.isObject(callObj) && config.isRestangularized(callObj)) {
callObj = stripRestangular(callObj);
}
var request = config.fullRequestInterceptor(callObj, operation, route, fetchUrl,
headers || {}, resParams || {}, this[config.restangularFields.httpConfig] || {});
var filledObject = {};
filledObject = config.transformElem(filledObject, false, route, service);
var okCallback = function(response) {
var resData = response.data;
var fullParams = response.config.params;
var elem = parseResponse(resData, operation, route, fetchUrl, response, deferred);
// accept 0 as response
if (elem !== null && elem !== undefined && elem !== '') {
var data;
if (true === config.plainByDefault) {
return resolvePromise(deferred, response, elem, filledObject);
}
if (operation === 'post' && !__this[config.restangularFields.restangularCollection]) {
data = restangularizeElem(
__this[config.restangularFields.parentResource],
elem,
route,
true,
null,
fullParams
);
resolvePromise(deferred, response, data, filledObject);
} else {
data = restangularizeElem(
__this[config.restangularFields.parentResource],
elem,
__this[config.restangularFields.route],
true,
null,
fullParams
);
data[config.restangularFields.singleOne] = __this[config.restangularFields.singleOne];
resolvePromise(deferred, response, data, filledObject);
}
} else {
resolvePromise(deferred, response, undefined, filledObject);
}
};
var errorCallback = function(response) {
if (response.status === 304 && config.isSafe(operation)) {
resolvePromise(deferred, response, __this, filledObject);
} else if (_.every(config.errorInterceptors, function(cb) {
return cb(response, deferred, okCallback) !== false;
})) {
// triggered if no callback returns false
deferred.reject(response);
}
};
// Overriding HTTP Method
var callOperation = operation;
var callHeaders = _.extend({}, request.headers);
var isOverrideOperation = config.isOverridenMethod(operation);
if (isOverrideOperation) {
callOperation = 'post';
callHeaders = _.extend(callHeaders, {
'X-HTTP-Method-Override': operation === 'remove' ? 'DELETE' : operation.toUpperCase()
});
} else if (config.jsonp && callOperation === 'get') {
callOperation = 'jsonp';
}
if (config.isSafe(operation)) {
if (isOverrideOperation) {
urlHandler.resource(this, $http, request.httpConfig, callHeaders, request.params,
what, etag, callOperation)[callOperation]({}).then(okCallback, errorCallback);
} else {
urlHandler.resource(this, $http, request.httpConfig, callHeaders, request.params,
what, etag, callOperation)[callOperation]().then(okCallback, errorCallback);
}
} else {
urlHandler.resource(this, $http, request.httpConfig, callHeaders, request.params,
what, etag, callOperation)[callOperation](request.element).then(okCallback, errorCallback);
}
return restangularizePromise(deferred.promise, false, filledObject);
}
function getFunction(params, headers) {
return _.bind(elemFunction, this)('get', undefined, params, undefined, headers);
}
function deleteFunction(params, headers) {
return _.bind(elemFunction, this)('remove', undefined, params, undefined, headers);
}
function putFunction(params, headers) {
return _.bind(elemFunction, this)('put', undefined, params, undefined, headers);
}
function postFunction(what, elem, params, headers) {
return _.bind(elemFunction, this)('post', what, params, elem, headers);
}
function headFunction(params, headers) {
return _.bind(elemFunction, this)('head', undefined, params, undefined, headers);
}
function traceFunction(params, headers) {
return _.bind(elemFunction, this)('trace', undefined, params, undefined, headers);
}
function optionsFunction(params, headers) {
return _.bind(elemFunction, this)('options', undefined, params, undefined, headers);
}
function patchFunction(elem, params, headers) {
return _.bind(elemFunction, this)('patch', undefined, params, elem, headers);
}
function customFunction(operation, path, params, headers, elem) {
return _.bind(elemFunction, this)(operation, path, params, elem, headers);
}
function addRestangularMethodFunction(name, operation, path, defaultParams, defaultHeaders, defaultElem) {
var bindedFunction;
if (operation === 'getList') {
bindedFunction = _.bind(fetchFunction, this, path);
} else {
bindedFunction = _.bind(customFunction, this, operation, path);
}
var createdFunction = function(params, headers, elem) {
var callParams = _.defaults({
params: params,
headers: headers,
elem: elem
}, {
params: defaultParams,
headers: defaultHeaders,
elem: defaultElem
});
return bindedFunction(callParams.params, callParams.headers, callParams.elem);
};
if (config.isSafe(operation)) {
this[name] = createdFunction;
} else {
this[name] = function(elem, params, headers) {
return createdFunction(params, headers, elem);
};
}
}
function withConfigurationFunction(configurer) {
var newConfig = angular.copy(_.omit(config, 'configuration'));
Configurer.init(newConfig, newConfig);
configurer(newConfig);
return createServiceForConfiguration(newConfig);
}
function toService(route, parent) {
var knownCollectionMethods = _.values(config.restangularFields);
var serv = {};
var collection = (parent || service).all(route);
serv.one = _.bind(one, (parent || service), parent, route);
serv.post = _.bind(collection.post, collection);
serv.getList = _.bind(collection.getList, collection);
serv.withHttpConfig = _.bind(collection.withHttpConfig, collection);
serv.get = _.bind(collection.get, collection);
for (var prop in collection) {
if (collection.hasOwnProperty(prop) && _.isFunction(collection[prop]) && !_.includes(knownCollectionMethods, prop)) {
serv[prop] = _.bind(collection[prop], collection);
}
}
return serv;
}
Configurer.init(service, config);
service.copy = _.bind(copyRestangularizedElement, service);
service.service = _.bind(toService, service);
service.withConfig = _.bind(withConfigurationFunction, service);
service.one = _.bind(one, service, null);
service.all = _.bind(all, service, null);
service.several = _.bind(several, service, null);
service.oneUrl = _.bind(oneUrl, service, null);
service.allUrl = _.bind(allUrl, service, null);
service.stripRestangular = _.bind(stripRestangular, service);
service.restangularizeElement = _.bind(restangularizeElem, service);
service.restangularizeCollection = _.bind(restangularizeCollectionAndElements, service);
return service;
}
return createServiceForConfiguration(globalConfiguration);
}];
});
return restangular.name;
}));
================================================
FILE: karma.conf.js
================================================
// Karma configuration
var angularVersion = '1.5.9';
var lodashVersion = '4.17.2';
module.exports = function (config) {
config.set({
// base path, that will be used to resolve files and exclude
basePath: '',
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
'https://cdnjs.cloudflare.com/ajax/libs/angular.js/' + angularVersion + '/angular.js',
'https://cdnjs.cloudflare.com/ajax/libs/angular.js/' + angularVersion + '/angular-mocks.js',
'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/' + lodashVersion + '/lodash.js',
'src/restangular.js',
'test/*.js'
],
// list of files to exclude
exclude: [
],
// test results reporter to use
// possible values: 'dots', 'progress', 'junit'
reporters: ['mocha', 'coverage'],
// web server port
port: 9876,
// cli runner port
runnerPort: 9100,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera
// - Safari (only Mac)
// - PhantomJS
// - IE (only Windows)
browsers: ['PhantomJS'],
// If browser does not capture in given timeout [ms], kill it
captureTimeout: 60000,
// Continuous Integration mode
// if true, it capture browsers, run tests and exit
singleRun: false,
preprocessors: {
// source files, that you wanna generate coverage for
// do not include tests or libraries
// (these files will be instrumented by Istanbul)
'src/**/*.js': ['coverage']
},
// optionally, configure the reporter
coverageReporter: {
type: 'lcov',
dir : 'coverage/'
}
});
};
================================================
FILE: karma.underscore.conf.js
================================================
// Karma configuration for use with underscore instead of lodash
var angularVersion = '1.5.9';
var underscoreVersion = '1.8.3';
module.exports = function (config) {
config.set({
// base path, that will be used to resolve files and exclude
basePath: '',
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
'https://cdnjs.cloudflare.com/ajax/libs/angular.js/' + angularVersion + '/angular.js',
'https://cdnjs.cloudflare.com/ajax/libs/angular.js/' + angularVersion + '/angular-mocks.js',
'https://cdnjs.cloudflare.com/ajax/libs/underscore.js/' + underscoreVersion + '/underscore.js',
'src/restangular.js',
'test/*.js'
],
// list of files to exclude
exclude: [
],
// test results reporter to use
// possible values: 'dots', 'progress', 'junit'
reporters: ['progress'],
// web server port
port: 9877,
// cli runner port
runnerPort: 9101,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera
// - Safari (only Mac)
// - PhantomJS
// - IE (only Windows)
browsers: ['PhantomJS'],
// If browser does not capture in given timeout [ms], kill it
captureTimeout: 60000,
// Continuous Integration mode
// if true, it capture browsers, run tests and exit
singleRun: false
});
};
================================================
FILE: license.md
================================================
The MIT License
Copyright (c) 2013-2015 Martin Gontovnikas http://www.gon.to/
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: package.json
================================================
{
"name": "restangular",
"description": "Restful Resources service for AngularJS apps",
"version": "1.6.1",
"filename": "restangular.min.js",
"main": "./dist/restangular.js",
"homepage": "https://github.com/mgonto/restangular",
"author": "Martin Gontovnikas ",
"repository": {
"type": "git",
"url": "git://github.com/mgonto/restangular.git"
},
"engines": {
"node": ">= 0.9"
},
"keywords": [
"angular",
"client",
"browser",
"restful",
"resources",
"rest",
"api"
],
"maintainers": [
{
"name": "Martin Gontovnikas",
"website": "http://gon.to/"
}
],
"dependencies": {
"lodash": "~4.17.0"
},
"peerDependencies": {
"angular": ">=1.3.12"
},
"devDependencies": {
"angular-mocks": "^1.4.8",
"grunt": "^1.0.0",
"grunt-bower": "*",
"grunt-bower-task": "*",
"grunt-cli": "^1.2.0",
"grunt-contrib-concat": "*",
"grunt-contrib-jshint": "*",
"grunt-contrib-uglify": "*",
"grunt-conventional-changelog": "latest",
"grunt-coveralls": "^1.0.1",
"grunt-karma": "latest",
"grunt-zip": "*",
"jasmine-core": "^2.5.2",
"karma": "^1.3.0",
"karma-chrome-launcher": "~2.0.0",
"karma-coverage": "^1.1.1",
"karma-firefox-launcher": "~v1.0.0",
"karma-jasmine": "^1.1.0",
"karma-mocha-reporter": "^2.2.0",
"karma-phantomjs-launcher": "~v1.0.2"
},
"scripts": {
"test": "grunt test --verbose"
},
"license": "MIT"
}
================================================
FILE: src/restangular.js
================================================
(function(root, factory) {
/* global define, require */
// https://github.com/umdjs/umd/blob/master/templates/returnExports.js
if (typeof define === 'function' && define.amd) {
define(['lodash', 'angular'], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(require('lodash'), require('angular'));
} else {
// No global export, Restangular will register itself as Angular.js module
factory(root._, root.angular);
}
}(this, function(_, angular) {
var restangular = angular.module('restangular', []);
restangular.provider('Restangular', function() {
// Configuration
var Configurer = {};
Configurer.init = function(object, config) {
object.configuration = config;
/**
* Those are HTTP safe methods for which there is no need to pass any data with the request.
*/
var safeMethods = ['get', 'head', 'options', 'trace', 'getlist'];
config.isSafe = function(operation) {
return _.includes(safeMethods, operation.toLowerCase());
};
var absolutePattern = /^https?:\/\//i;
config.isAbsoluteUrl = function(string) {
return _.isUndefined(config.absoluteUrl) || _.isNull(config.absoluteUrl) ?
string && absolutePattern.test(string) :
config.absoluteUrl;
};
config.absoluteUrl = _.isUndefined(config.absoluteUrl) ? true : config.absoluteUrl;
object.setSelfLinkAbsoluteUrl = function(value) {
config.absoluteUrl = value;
};
/**
* This is the BaseURL to be used with Restangular
*/
config.baseUrl = _.isUndefined(config.baseUrl) ? '' : config.baseUrl;
object.setBaseUrl = function(newBaseUrl) {
config.baseUrl = /\/$/.test(newBaseUrl) ?
newBaseUrl.substring(0, newBaseUrl.length - 1) :
newBaseUrl;
return this;
};
/**
* Sets the extra fields to keep from the parents
*/
config.extraFields = config.extraFields || [];
object.setExtraFields = function(newExtraFields) {
config.extraFields = newExtraFields;
return this;
};
/**
* Some default $http parameter to be used in EVERY call
**/
config.defaultHttpFields = config.defaultHttpFields || {};
object.setDefaultHttpFields = function(values) {
config.defaultHttpFields = values;
return this;
};
/**
* Always return plain data, no restangularized object
**/
config.plainByDefault = config.plainByDefault || false;
object.setPlainByDefault = function(value) {
config.plainByDefault = value === true ? true : false;
return this;
};
config.withHttpValues = function(httpLocalConfig, obj) {
return _.defaults(obj, httpLocalConfig, config.defaultHttpFields);
};
config.encodeIds = _.isUndefined(config.encodeIds) ? true : config.encodeIds;
object.setEncodeIds = function(encode) {
config.encodeIds = encode;
};
config.defaultRequestParams = config.defaultRequestParams || {
get: {},
post: {},
put: {},
remove: {},
common: {}
};
object.setDefaultRequestParams = function(param1, param2) {
var methods = [],
params = param2 || param1;
if (!_.isUndefined(param2)) {
if (_.isArray(param1)) {
methods = param1;
} else {
methods.push(param1);
}
} else {
methods.push('common');
}
_.each(methods, function(method) {
config.defaultRequestParams[method] = params;
});
return this;
};
object.requestParams = config.defaultRequestParams;
config.defaultHeaders = config.defaultHeaders || {};
object.setDefaultHeaders = function(headers) {
config.defaultHeaders = headers;
object.defaultHeaders = config.defaultHeaders;
return this;
};
object.defaultHeaders = config.defaultHeaders;
/**
* Method overriders will set which methods are sent via POST with an X-HTTP-Method-Override
**/
config.methodOverriders = config.methodOverriders || [];
object.setMethodOverriders = function(values) {
var overriders = _.extend([], values);
if (config.isOverridenMethod('delete', overriders)) {
overriders.push('remove');
}
config.methodOverriders = overriders;
return this;
};
config.jsonp = _.isUndefined(config.jsonp) ? false : config.jsonp;
object.setJsonp = function(active) {
config.jsonp = active;
};
config.isOverridenMethod = function(method, values) {
var search = values || config.methodOverriders;
return !_.isUndefined(_.find(search, function(one) {
return one.toLowerCase() === method.toLowerCase();
}));
};
/**
* Sets the URL creator type. For now, only Path is created. In the future we'll have queryParams
**/
config.urlCreator = config.urlCreator || 'path';
object.setUrlCreator = function(name) {
if (!_.has(config.urlCreatorFactory, name)) {
throw new Error('URL Path selected isn\'t valid');
}
config.urlCreator = name;
return this;
};
/**
* You can set the restangular fields here. The 3 required fields for Restangular are:
*
* id: Id of the element
* route: name of the route of this element
* parentResource: the reference to the parent resource
*
* All of this fields except for id, are handled (and created) by Restangular. By default,
* the field values will be id, route and parentResource respectively
*/
config.restangularFields = config.restangularFields || {
id: 'id',
route: 'route',
parentResource: 'parentResource',
restangularCollection: 'restangularCollection',
cannonicalId: '__cannonicalId',
etag: 'restangularEtag',
selfLink: 'href',
get: 'get',
getList: 'getList',
put: 'put',
post: 'post',
remove: 'remove',
head: 'head',
trace: 'trace',
options: 'options',
patch: 'patch',
getRestangularUrl: 'getRestangularUrl',
getRequestedUrl: 'getRequestedUrl',
putElement: 'putElement',
addRestangularMethod: 'addRestangularMethod',
getParentList: 'getParentList',
clone: 'clone',
ids: 'ids',
httpConfig: '_$httpConfig',
reqParams: 'reqParams',
one: 'one',
all: 'all',
several: 'several',
oneUrl: 'oneUrl',
allUrl: 'allUrl',
customPUT: 'customPUT',
customPATCH: 'customPATCH',
customPOST: 'customPOST',
customDELETE: 'customDELETE',
customGET: 'customGET',
customGETLIST: 'customGETLIST',
customOperation: 'customOperation',
doPUT: 'doPUT',
doPATCH: 'doPATCH',
doPOST: 'doPOST',
doDELETE: 'doDELETE',
doGET: 'doGET',
doGETLIST: 'doGETLIST',
fromServer: 'fromServer',
withConfig: 'withConfig',
withHttpConfig: 'withHttpConfig',
singleOne: 'singleOne',
plain: 'plain',
save: 'save',
restangularized: 'restangularized'
};
object.setRestangularFields = function(resFields) {
config.restangularFields =
_.extend(config.restangularFields, resFields);
return this;
};
config.isRestangularized = function(obj) {
return !!obj[config.restangularFields.restangularized];
};
config.setFieldToElem = function(field, elem, value) {
var properties = field.split('.');
var idValue = elem;
_.each(_.initial(properties), function(prop) {
idValue[prop] = {};
idValue = idValue[prop];
});
idValue[_.last(properties)] = value;
return this;
};
config.getFieldFromElem = function(field, elem) {
var properties = field.split('.');
var idValue = elem;
_.each(properties, function(prop) {
if (idValue) {
idValue = idValue[prop];
}
});
return angular.copy(idValue);
};
config.setIdToElem = function(elem, id /*, route */ ) {
config.setFieldToElem(config.restangularFields.id, elem, id);
return this;
};
config.getIdFromElem = function(elem) {
return config.getFieldFromElem(config.restangularFields.id, elem);
};
config.isValidId = function(elemId) {
return '' !== elemId && !_.isUndefined(elemId) && !_.isNull(elemId);
};
config.setUrlToElem = function(elem, url /*, route */ ) {
config.setFieldToElem(config.restangularFields.selfLink, elem, url);
return this;
};
config.getUrlFromElem = function(elem) {
return config.getFieldFromElem(config.restangularFields.selfLink, elem);
};
config.useCannonicalId = _.isUndefined(config.useCannonicalId) ? false : config.useCannonicalId;
object.setUseCannonicalId = function(value) {
config.useCannonicalId = value;
return this;
};
config.getCannonicalIdFromElem = function(elem) {
var cannonicalId = elem[config.restangularFields.cannonicalId];
var actualId = config.isValidId(cannonicalId) ? cannonicalId : config.getIdFromElem(elem);
return actualId;
};
/**
* Sets the Response parser. This is used in case your response isn't directly the data.
* For example if you have a response like {meta: {'meta'}, data: {name: 'Gonto'}}
* you can extract this data which is the one that needs wrapping
*
* The ResponseExtractor is a function that receives the response and the method executed.
*/
config.responseInterceptors = config.responseInterceptors || [];
config.defaultResponseInterceptor = function(data /*, operation, what, url, response, deferred */ ) {
return data;
};
config.responseExtractor = function(data, operation, what, url, response, deferred) {
var interceptors = angular.copy(config.responseInterceptors);
interceptors.push(config.defaultResponseInterceptor);
var theData = data;
_.each(interceptors, function(interceptor) {
theData = interceptor(theData, operation,
what, url, response, deferred);
});
return theData;
};
object.addResponseInterceptor = function(extractor) {
config.responseInterceptors.push(extractor);
return this;
};
config.errorInterceptors = config.errorInterceptors || [];
object.addErrorInterceptor = function(interceptor) {
config.errorInterceptors.push(interceptor);
return this;
};
object.setResponseInterceptor = object.addResponseInterceptor;
object.setResponseExtractor = object.addResponseInterceptor;
object.setErrorInterceptor = object.addErrorInterceptor;
/**
* Response interceptor is called just before resolving promises.
*/
/**
* Request interceptor is called before sending an object to the server.
*/
config.requestInterceptors = config.requestInterceptors || [];
config.defaultInterceptor = function(element, operation, path, url, headers, params, httpConfig) {
return {
element: element,
headers: headers,
params: params,
httpConfig: httpConfig
};
};
config.fullRequestInterceptor = function(element, operation, path, url, headers, params, httpConfig) {
var interceptors = angular.copy(config.requestInterceptors);
var defaultRequest = config.defaultInterceptor(element, operation, path, url, headers, params, httpConfig);
return _.reduce(interceptors, function(request, interceptor) {
return _.extend(request, interceptor(request.element, operation,
path, url, request.headers, request.params, request.httpConfig));
}, defaultRequest);
};
object.addRequestInterceptor = function(interceptor) {
config.requestInterceptors.push(function(elem, operation, path, url, headers, params, httpConfig) {
return {
headers: headers,
params: params,
element: interceptor(elem, operation, path, url),
httpConfig: httpConfig
};
});
return this;
};
object.setRequestInterceptor = object.addRequestInterceptor;
object.addFullRequestInterceptor = function(interceptor) {
config.requestInterceptors.push(interceptor);
return this;
};
object.setFullRequestInterceptor = object.addFullRequestInterceptor;
config.onBeforeElemRestangularized = config.onBeforeElemRestangularized || function(elem) {
return elem;
};
object.setOnBeforeElemRestangularized = function(post) {
config.onBeforeElemRestangularized = post;
return this;
};
object.setRestangularizePromiseInterceptor = function(interceptor) {
config.restangularizePromiseInterceptor = interceptor;
return this;
};
/**
* This method is called after an element has been "Restangularized".
*
* It receives the element, a boolean indicating if it's an element or a collection
* and the name of the model
*
*/
config.onElemRestangularized = config.onElemRestangularized || function(elem) {
return elem;
};
object.setOnElemRestangularized = function(post) {
config.onElemRestangularized = post;
return this;
};
config.shouldSaveParent = config.shouldSaveParent || function() {
return true;
};
object.setParentless = function(values) {
if (_.isArray(values)) {
config.shouldSaveParent = function(route) {
return !_.includes(values, route);
};
} else if (_.isBoolean(values)) {
config.shouldSaveParent = function() {
return !values;
};
}
return this;
};
/**
* This lets you set a suffix to every request.
*
* For example, if your api requires that for JSon requests you do /users/123.json, you can set that
* in here.
*
*
* By default, the suffix is null
*/
config.suffix = _.isUndefined(config.suffix) ? null : config.suffix;
object.setRequestSuffix = function(newSuffix) {
config.suffix = newSuffix;
return this;
};
/**
* Add element transformers for certain routes.
*/
config.transformers = config.transformers || {};
config.matchTransformers = config.matchTransformers || [];
object.addElementTransformer = function(type, secondArg, thirdArg) {
var isCollection = null;
var transformer = null;
if (arguments.length === 2) {
transformer = secondArg;
} else {
transformer = thirdArg;
isCollection = secondArg;
}
var transformerFn = function(coll, elem) {
if (_.isNull(isCollection) || (coll === isCollection)) {
return transformer(elem);
}
return elem;
};
if (_.isRegExp(type)) {
config.matchTransformers.push({
regexp: type,
transformer: transformerFn
});
} else {
if (!config.transformers[type]) {
config.transformers[type] = [];
}
config.transformers[type].push(transformerFn);
}
return object;
};
object.extendCollection = function(route, fn) {
return object.addElementTransformer(route, true, fn);
};
object.extendModel = function(route, fn) {
return object.addElementTransformer(route, false, fn);
};
config.transformElem = function(elem, isCollection, route, Restangular, force) {
if (!force && !config.transformLocalElements && !elem[config.restangularFields.fromServer]) {
return elem;
}
var changedElem = elem;
var matchTransformers = config.matchTransformers;
if (matchTransformers) {
_.each(matchTransformers, function(transformer) {
if (transformer.regexp.test(route)) {
changedElem = transformer.transformer(isCollection, changedElem);
}
});
}
var typeTransformers = config.transformers[route];
if (typeTransformers) {
_.each(typeTransformers, function(transformer) {
changedElem = transformer(isCollection, changedElem);
});
}
return config.onElemRestangularized(changedElem, isCollection, route, Restangular);
};
config.transformLocalElements = _.isUndefined(config.transformLocalElements) ?
false :
config.transformLocalElements;
object.setTransformOnlyServerElements = function(active) {
config.transformLocalElements = !active;
};
config.fullResponse = _.isUndefined(config.fullResponse) ? false : config.fullResponse;
object.setFullResponse = function(full) {
config.fullResponse = full;
return this;
};
//Internal values and functions
config.urlCreatorFactory = {};
/**
* Base URL Creator. Base prototype for everything related to it
**/
var BaseCreator = function() {};
BaseCreator.prototype.setConfig = function(config) {
this.config = config;
return this;
};
BaseCreator.prototype.parentsArray = function(current) {
var parents = [];
while (current) {
parents.push(current);
current = current[this.config.restangularFields.parentResource];
}
return parents.reverse();
};
function RestangularResource(config, $http, url, configurer) {
var resource = {};
_.each(_.keys(configurer), function(key) {
var value = configurer[key];
// Add default parameters
value.params = _.extend({}, value.params, config.defaultRequestParams[value.method.toLowerCase()]);
// We don't want the ? if no params are there
if (_.isEmpty(value.params)) {
delete value.params;
}
if (config.isSafe(value.method)) {
resource[key] = function() {
return $http(_.extend(value, {
url: url
}));
};
} else {
resource[key] = function(data) {
return $http(_.extend(value, {
url: url,
data: data
}));
};
}
});
return resource;
}
BaseCreator.prototype.resource = function(current, $http, localHttpConfig, callHeaders, callParams, what, etag, operation) {
var params = _.defaults(callParams || {}, this.config.defaultRequestParams.common);
var headers = _.defaults(callHeaders || {}, this.config.defaultHeaders);
if (etag) {
if (!config.isSafe(operation)) {
headers['If-Match'] = etag;
} else {
headers['If-None-Match'] = etag;
}
}
var url = this.base(current);
if (what || what === 0) {
var add = '';
if (!/\/$/.test(url)) {
add += '/';
}
add += what;
url += add;
}
if (this.config.suffix &&
url.indexOf(this.config.suffix, url.length - this.config.suffix.length) === -1 &&
!this.config.getUrlFromElem(current)) {
url += this.config.suffix;
}
current[this.config.restangularFields.httpConfig] = undefined;
return RestangularResource(this.config, $http, url, {
getList: this.config.withHttpValues(localHttpConfig, {
method: 'GET',
params: params,
headers: headers
}),
get: this.config.withHttpValues(localHttpConfig, {
method: 'GET',
params: params,
headers: headers
}),
jsonp: this.config.withHttpValues(localHttpConfig, {
method: 'jsonp',
params: params,
headers: headers
}),
put: this.config.withHttpValues(localHttpConfig, {
method: 'PUT',
params: params,
headers: headers
}),
post: this.config.withHttpValues(localHttpConfig, {
method: 'POST',
params: params,
headers: headers
}),
remove: this.config.withHttpValues(localHttpConfig, {
method: 'DELETE',
params: params,
headers: headers
}),
head: this.config.withHttpValues(localHttpConfig, {
method: 'HEAD',
params: params,
headers: headers
}),
trace: this.config.withHttpValues(localHttpConfig, {
method: 'TRACE',
params: params,
headers: headers
}),
options: this.config.withHttpValues(localHttpConfig, {
method: 'OPTIONS',
params: params,
headers: headers
}),
patch: this.config.withHttpValues(localHttpConfig, {
method: 'PATCH',
params: params,
headers: headers
})
});
};
/**
* This is the Path URL creator. It uses Path to show Hierarchy in the Rest API.
* This means that if you have an Account that then has a set of Buildings, a URL to a building
* would be /accounts/123/buildings/456
**/
var Path = function() {};
Path.prototype = new BaseCreator();
Path.prototype.normalizeUrl = function(url) {
var parts = /((?:http[s]?:)?\/\/)?(.*)?/.exec(url);
parts[2] = parts[2].replace(/[\\\/]+/g, '/');
return (typeof parts[1] !== 'undefined') ? parts[1] + parts[2] : parts[2];
};
Path.prototype.base = function(current) {
var __this = this;
return _.reduce(this.parentsArray(current), function(acum, elem) {
var elemUrl;
var elemSelfLink = __this.config.getUrlFromElem(elem);
if (elemSelfLink) {
if (__this.config.isAbsoluteUrl(elemSelfLink)) {
return elemSelfLink;
} else {
elemUrl = elemSelfLink;
}
} else {
elemUrl = elem[__this.config.restangularFields.route];
if (elem[__this.config.restangularFields.restangularCollection]) {
var ids = elem[__this.config.restangularFields.ids];
if (ids) {
elemUrl += '/' + ids.join(',');
}
} else {
var elemId;
if (__this.config.useCannonicalId) {
elemId = __this.config.getCannonicalIdFromElem(elem);
} else {
elemId = __this.config.getIdFromElem(elem);
}
if (config.isValidId(elemId) && !elem.singleOne) {
elemUrl += '/' + (__this.config.encodeIds ? encodeURIComponent(elemId) : elemId);
}
}
}
acum = acum.replace(/\/$/, '') + '/' + elemUrl;
return __this.normalizeUrl(acum);
}, this.config.baseUrl);
};
Path.prototype.fetchUrl = function(current, what) {
var baseUrl = this.base(current);
if (what) {
baseUrl += '/' + what;
}
return baseUrl;
};
Path.prototype.fetchRequestedUrl = function(current, what) {
var url = this.fetchUrl(current, what);
var params = current[config.restangularFields.reqParams];
// From here on and until the end of fetchRequestedUrl,
// the code has been kindly borrowed from angular.js
// The reason for such code bloating is coherence:
// If the user were to use this for cache management, the
// serialization of parameters would need to be identical
// to the one done by angular for cache keys to match.
function sortedKeys(obj) {
var keys = [];
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
keys.push(key);
}
}
return keys.sort();
}
function forEachSorted(obj, iterator, context) {
var keys = sortedKeys(obj);
for (var i = 0; i < keys.length; i++) {
iterator.call(context, obj[keys[i]], keys[i]);
}
return keys;
}
function encodeUriQuery(val, pctEncodeSpaces) {
return encodeURIComponent(val).
replace(/%40/gi, '@').
replace(/%3A/gi, ':').
replace(/%24/g, '$').
replace(/%2C/gi, ',').
replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
}
if (!params) {
return url + (this.config.suffix || '');
}
var parts = [];
forEachSorted(params, function(value, key) {
if (value === null || value === undefined) {
return;
}
if (!angular.isArray(value)) {
value = [value];
}
angular.forEach(value, function(v) {
if (angular.isObject(v)) {
v = angular.toJson(v);
}
parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(v));
});
});
return url + (this.config.suffix || '') + ((url.indexOf('?') === -1) ? '?' : '&') + parts.join('&');
};
config.urlCreatorFactory.path = Path;
};
var globalConfiguration = {};
Configurer.init(this, globalConfiguration);
this.$get = ['$http', '$q', function($http, $q) {
function createServiceForConfiguration(config) {
var service = {};
var urlHandler = new config.urlCreatorFactory[config.urlCreator]();
urlHandler.setConfig(config);
function restangularizeBase(parent, elem, route, reqParams, fromServer) {
elem[config.restangularFields.route] = route;
elem[config.restangularFields.getRestangularUrl] = _.bind(urlHandler.fetchUrl, urlHandler, elem);
elem[config.restangularFields.getRequestedUrl] = _.bind(urlHandler.fetchRequestedUrl, urlHandler, elem);
elem[config.restangularFields.addRestangularMethod] = _.bind(addRestangularMethodFunction, elem);
elem[config.restangularFields.clone] = _.bind(copyRestangularizedElement, elem, elem);
elem[config.restangularFields.reqParams] = _.isEmpty(reqParams) ? null : reqParams;
elem[config.restangularFields.withHttpConfig] = _.bind(withHttpConfig, elem);
elem[config.restangularFields.plain] = _.bind(stripRestangular, elem, elem);
// Tag element as restangularized
elem[config.restangularFields.restangularized] = true;
// RequestLess connection
elem[config.restangularFields.one] = _.bind(one, elem, elem);
elem[config.restangularFields.all] = _.bind(all, elem, elem);
elem[config.restangularFields.several] = _.bind(several, elem, elem);
elem[config.restangularFields.oneUrl] = _.bind(oneUrl, elem, elem);
elem[config.restangularFields.allUrl] = _.bind(allUrl, elem, elem);
elem[config.restangularFields.fromServer] = !!fromServer;
if (parent && config.shouldSaveParent(route)) {
var parentId = config.getIdFromElem(parent);
var parentUrl = config.getUrlFromElem(parent);
var restangularFieldsForParent = _.union(
_.values(_.pick(config.restangularFields, ['route', 'singleOne', 'parentResource'])),
config.extraFields
);
var parentResource = _.pick(parent, restangularFieldsForParent);
if (config.isValidId(parentId)) {
config.setIdToElem(parentResource, parentId, route);
}
if (config.isValidId(parentUrl)) {
config.setUrlToElem(parentResource, parentUrl, route);
}
elem[config.restangularFields.parentResource] = parentResource;
} else {
elem[config.restangularFields.parentResource] = null;
}
return elem;
}
function one(parent, route, id, singleOne) {
var error;
if (_.isNumber(route) || _.isNumber(parent)) {
error = 'You\'re creating a Restangular entity with the number ';
error += 'instead of the route or the parent. For example, you can\'t call .one(12).';
throw new Error(error);
}
if (_.isUndefined(route)) {
error = 'You\'re creating a Restangular entity either without the path. ';
error += 'For example you can\'t call .one(). Please check if your arguments are valid.';
throw new Error(error);
}
var elem = {};
config.setIdToElem(elem, id, route);
config.setFieldToElem(config.restangularFields.singleOne, elem, singleOne);
return restangularizeElem(parent, elem, route, false);
}
function all(parent, route) {
return restangularizeCollection(parent, [], route, false);
}
function several(parent, route /*, ids */ ) {
var collection = [];
collection[config.restangularFields.ids] = Array.prototype.splice.call(arguments, 2);
return restangularizeCollection(parent, collection, route, false);
}
function oneUrl(parent, route, url) {
if (!route) {
throw new Error('Route is mandatory when creating new Restangular objects.');
}
var elem = {};
config.setUrlToElem(elem, url, route);
return restangularizeElem(parent, elem, route, false);
}
function allUrl(parent, route, url) {
if (!route) {
throw new Error('Route is mandatory when creating new Restangular objects.');
}
var elem = {};
config.setUrlToElem(elem, url, route);
return restangularizeCollection(parent, elem, route, false);
}
// Promises
function restangularizePromise(promise, isCollection, valueToFill) {
promise.call = _.bind(promiseCall, promise);
promise.get = _.bind(promiseGet, promise);
promise[config.restangularFields.restangularCollection] = isCollection;
if (isCollection) {
promise.push = _.bind(promiseCall, promise, 'push');
}
promise.$object = valueToFill;
if (config.restangularizePromiseInterceptor) {
config.restangularizePromiseInterceptor(promise);
}
return promise;
}
function promiseCall(method) {
var deferred = $q.defer();
var callArgs = arguments;
var filledValue = {};
this.then(function(val) {
var params = Array.prototype.slice.call(callArgs, 1);
var func = val[method];
func.apply(val, params);
filledValue = val;
deferred.resolve(val);
});
return restangularizePromise(deferred.promise, this[config.restangularFields.restangularCollection], filledValue);
}
function promiseGet(what) {
var deferred = $q.defer();
var filledValue = {};
this.then(function(val) {
filledValue = val[what];
deferred.resolve(filledValue);
});
return restangularizePromise(deferred.promise, this[config.restangularFields.restangularCollection], filledValue);
}
function resolvePromise(deferred, response, data, filledValue) {
_.extend(filledValue, data);
// Trigger the full response interceptor.
if (config.fullResponse) {
return deferred.resolve(_.extend(response, {
data: data
}));
} else {
deferred.resolve(data);
}
}
// Elements
function stripRestangular(elem) {
if (_.isArray(elem)) {
var array = [];
_.each(elem, function(value) {
array.push(config.isRestangularized(value) ? stripRestangular(value) : value);
});
return array;
} else {
return _.omit(elem, _.values(_.omit(config.restangularFields, 'id')));
}
}
function addCustomOperation(elem) {
elem[config.restangularFields.customOperation] = _.bind(customFunction, elem);
var requestMethods = {
get: customFunction,
delete: customFunction
};
_.each(['put', 'patch', 'post'], function(name) {
requestMethods[name] = function(operation, elem, path, params, headers) {
return _.bind(customFunction, this)(operation, path, params, headers, elem);
};
});
_.each(requestMethods, function(requestFunc, name) {
var callOperation = name === 'delete' ? 'remove' : name;
_.each(['do', 'custom'], function(alias) {
elem[config.restangularFields[alias + name.toUpperCase()]] = _.bind(requestFunc, elem, callOperation);
});
});
elem[config.restangularFields.customGETLIST] = _.bind(fetchFunction, elem);
elem[config.restangularFields.doGETLIST] = elem[config.restangularFields.customGETLIST];
}
function copyRestangularizedElement(element) {
var copiedElement = angular.copy(element);
// check if we're dealing with a collection (i.e. an array)
// and restangularize the element using the proper restangularizer,
// element / collection
if (_.isArray(element)) {
return restangularizeCollection(
element[config.restangularFields.parentResource],
copiedElement,
element[config.restangularFields.route],
element[config.restangularFields.fromServer],
element[config.restangularFields.reqParams]
);
}
// not a collection, restangularize it as an element
return restangularizeElem(
element[config.restangularFields.parentResource],
copiedElement,
element[config.restangularFields.route],
element[config.restangularFields.fromServer],
element[config.restangularFields.restangularCollection],
element[config.restangularFields.reqParams]
);
}
function restangularizeElem(parent, element, route, fromServer, collection, reqParams) {
var elem = config.onBeforeElemRestangularized(element, false, route);
var localElem = restangularizeBase(parent, elem, route, reqParams, fromServer);
if (config.useCannonicalId) {
localElem[config.restangularFields.cannonicalId] = config.getIdFromElem(localElem);
}
if (collection) {
localElem[config.restangularFields.getParentList] = function() {
return collection;
};
}
localElem[config.restangularFields.restangularCollection] = false;
localElem[config.restangularFields.get] = _.bind(getFunction, localElem);
localElem[config.restangularFields.getList] = _.bind(fetchFunction, localElem);
localElem[config.restangularFields.put] = _.bind(putFunction, localElem);
localElem[config.restangularFields.post] = _.bind(postFunction, localElem);
localElem[config.restangularFields.remove] = _.bind(deleteFunction, localElem);
localElem[config.restangularFields.head] = _.bind(headFunction, localElem);
localElem[config.restangularFields.trace] = _.bind(traceFunction, localElem);
localElem[config.restangularFields.options] = _.bind(optionsFunction, localElem);
localElem[config.restangularFields.patch] = _.bind(patchFunction, localElem);
localElem[config.restangularFields.save] = _.bind(save, localElem);
addCustomOperation(localElem);
return config.transformElem(localElem, false, route, service, true);
}
function restangularizeCollection(parent, element, route, fromServer, reqParams) {
var elem = config.onBeforeElemRestangularized(element, true, route);
var localElem = restangularizeBase(parent, elem, route, reqParams, fromServer);
localElem[config.restangularFields.restangularCollection] = true;
localElem[config.restangularFields.post] = _.bind(postFunction, localElem, null);
localElem[config.restangularFields.remove] = _.bind(deleteFunction, localElem);
localElem[config.restangularFields.head] = _.bind(headFunction, localElem);
localElem[config.restangularFields.trace] = _.bind(traceFunction, localElem);
localElem[config.restangularFields.putElement] = _.bind(putElementFunction, localElem);
localElem[config.restangularFields.options] = _.bind(optionsFunction, localElem);
localElem[config.restangularFields.patch] = _.bind(patchFunction, localElem);
localElem[config.restangularFields.get] = _.bind(getById, localElem);
localElem[config.restangularFields.getList] = _.bind(fetchFunction, localElem, null);
addCustomOperation(localElem);
return config.transformElem(localElem, true, route, service, true);
}
function restangularizeCollectionAndElements(parent, element, route, fromServer) {
var collection = restangularizeCollection(parent, element, route, fromServer);
_.each(collection, function(elem) {
if (elem) {
restangularizeElem(parent, elem, route, fromServer);
}
});
return collection;
}
function getById(id, reqParams, headers) {
return this.customGET(id.toString(), reqParams, headers);
}
function putElementFunction(idx, params, headers) {
var __this = this;
var elemToPut = this[idx];
var deferred = $q.defer();
var filledArray = [];
filledArray = config.transformElem(filledArray, true, elemToPut[config.restangularFields.route], service);
elemToPut.put(params, headers).then(function(serverElem) {
var newArray = copyRestangularizedElement(__this);
newArray[idx] = serverElem;
filledArray = newArray;
deferred.resolve(newArray);
}, function(response) {
deferred.reject(response);
});
return restangularizePromise(deferred.promise, true, filledArray);
}
function parseResponse(resData, operation, route, fetchUrl, response, deferred) {
var data = config.responseExtractor(resData, operation, route, fetchUrl, response, deferred);
var etag = response.headers('ETag');
if (data && etag) {
data[config.restangularFields.etag] = etag;
}
return data;
}
function fetchFunction(what, reqParams, headers) {
var __this = this;
var deferred = $q.defer();
var operation = 'getList';
var url = urlHandler.fetchUrl(this, what);
var whatFetched = what || __this[config.restangularFields.route];
var request = config.fullRequestInterceptor(null, operation,
whatFetched, url, headers || {}, reqParams || {}, this[config.restangularFields.httpConfig] || {});
var filledArray = [];
filledArray = config.transformElem(filledArray, true, whatFetched, service);
var method = 'getList';
if (config.jsonp) {
method = 'jsonp';
}
var okCallback = function(response) {
var resData = response.data;
var fullParams = response.config.params;
var data = parseResponse(resData, operation, whatFetched, url, response, deferred);
// support empty response for getList() calls (some APIs respond with 204 and empty body)
if (_.isUndefined(data) || '' === data) {
data = [];
}
if (!_.isArray(data)) {
throw new Error('Response for getList SHOULD be an array and not an object or something else');
}
if (true === config.plainByDefault) {
return resolvePromise(deferred, response, data, filledArray);
}
var processedData = _.map(data, function(elem) {
if (!__this[config.restangularFields.restangularCollection]) {
return restangularizeElem(__this, elem, what, true, data);
} else {
return restangularizeElem(__this[config.restangularFields.parentResource],
elem, __this[config.restangularFields.route], true, data);
}
});
processedData = _.extend(data, processedData);
if (!__this[config.restangularFields.restangularCollection]) {
resolvePromise(
deferred,
response,
restangularizeCollection(
__this,
processedData,
what,
true,
fullParams
),
filledArray
);
} else {
resolvePromise(
deferred,
response,
restangularizeCollection(
__this[config.restangularFields.parentResource],
processedData,
__this[config.restangularFields.route],
true,
fullParams
),
filledArray
);
}
};
urlHandler.resource(this, $http, request.httpConfig, request.headers, request.params, what,
this[config.restangularFields.etag], operation)[method]().then(okCallback, function error(response) {
if (response.status === 304 && __this[config.restangularFields.restangularCollection]) {
resolvePromise(deferred, response, __this, filledArray);
} else if (_.every(config.errorInterceptors, function(cb) {
return cb(response, deferred, okCallback) !== false;
})) {
// triggered if no callback returns false
deferred.reject(response);
}
});
return restangularizePromise(deferred.promise, true, filledArray);
}
function withHttpConfig(httpConfig) {
this[config.restangularFields.httpConfig] = httpConfig;
return this;
}
function save(params, headers) {
if (this[config.restangularFields.fromServer]) {
return this[config.restangularFields.put](params, headers);
} else {
return _.bind(elemFunction, this)('post', undefined, params, undefined, headers);
}
}
function elemFunction(operation, what, params, obj, headers) {
var __this = this;
var deferred = $q.defer();
var resParams = params || {};
var route = what || this[config.restangularFields.route];
var fetchUrl = urlHandler.fetchUrl(this, what);
var callObj = obj || this;
// fallback to etag on restangular object (since for custom methods we probably don't explicitly specify the etag field)
var etag = callObj[config.restangularFields.etag] || (operation !== 'post' ? this[config.restangularFields.etag] : null);
if (_.isObject(callObj) && config.isRestangularized(callObj)) {
callObj = stripRestangular(callObj);
}
var request = config.fullRequestInterceptor(callObj, operation, route, fetchUrl,
headers || {}, resParams || {}, this[config.restangularFields.httpConfig] || {});
var filledObject = {};
filledObject = config.transformElem(filledObject, false, route, service);
var okCallback = function(response) {
var resData = response.data;
var fullParams = response.config.params;
var elem = parseResponse(resData, operation, route, fetchUrl, response, deferred);
// accept 0 as response
if (elem !== null && elem !== undefined && elem !== '') {
var data;
if (true === config.plainByDefault) {
return resolvePromise(deferred, response, elem, filledObject);
}
if (operation === 'post' && !__this[config.restangularFields.restangularCollection]) {
data = restangularizeElem(
__this[config.restangularFields.parentResource],
elem,
route,
true,
null,
fullParams
);
resolvePromise(deferred, response, data, filledObject);
} else {
data = restangularizeElem(
__this[config.restangularFields.parentResource],
elem,
__this[config.restangularFields.route],
true,
null,
fullParams
);
data[config.restangularFields.singleOne] = __this[config.restangularFields.singleOne];
resolvePromise(deferred, response, data, filledObject);
}
} else {
resolvePromise(deferred, response, undefined, filledObject);
}
};
var errorCallback = function(response) {
if (response.status === 304 && config.isSafe(operation)) {
resolvePromise(deferred, response, __this, filledObject);
} else if (_.every(config.errorInterceptors, function(cb) {
return cb(response, deferred, okCallback) !== false;
})) {
// triggered if no callback returns false
deferred.reject(response);
}
};
// Overriding HTTP Method
var callOperation = operation;
var callHeaders = _.extend({}, request.headers);
var isOverrideOperation = config.isOverridenMethod(operation);
if (isOverrideOperation) {
callOperation = 'post';
callHeaders = _.extend(callHeaders, {
'X-HTTP-Method-Override': operation === 'remove' ? 'DELETE' : operation.toUpperCase()
});
} else if (config.jsonp && callOperation === 'get') {
callOperation = 'jsonp';
}
if (config.isSafe(operation)) {
if (isOverrideOperation) {
urlHandler.resource(this, $http, request.httpConfig, callHeaders, request.params,
what, etag, callOperation)[callOperation]({}).then(okCallback, errorCallback);
} else {
urlHandler.resource(this, $http, request.httpConfig, callHeaders, request.params,
what, etag, callOperation)[callOperation]().then(okCallback, errorCallback);
}
} else {
urlHandler.resource(this, $http, request.httpConfig, callHeaders, request.params,
what, etag, callOperation)[callOperation](request.element).then(okCallback, errorCallback);
}
return restangularizePromise(deferred.promise, false, filledObject);
}
function getFunction(params, headers) {
return _.bind(elemFunction, this)('get', undefined, params, undefined, headers);
}
function deleteFunction(params, headers) {
return _.bind(elemFunction, this)('remove', undefined, params, undefined, headers);
}
function putFunction(params, headers) {
return _.bind(elemFunction, this)('put', undefined, params, undefined, headers);
}
function postFunction(what, elem, params, headers) {
return _.bind(elemFunction, this)('post', what, params, elem, headers);
}
function headFunction(params, headers) {
return _.bind(elemFunction, this)('head', undefined, params, undefined, headers);
}
function traceFunction(params, headers) {
return _.bind(elemFunction, this)('trace', undefined, params, undefined, headers);
}
function optionsFunction(params, headers) {
return _.bind(elemFunction, this)('options', undefined, params, undefined, headers);
}
function patchFunction(elem, params, headers) {
return _.bind(elemFunction, this)('patch', undefined, params, elem, headers);
}
function customFunction(operation, path, params, headers, elem) {
return _.bind(elemFunction, this)(operation, path, params, elem, headers);
}
function addRestangularMethodFunction(name, operation, path, defaultParams, defaultHeaders, defaultElem) {
var bindedFunction;
if (operation === 'getList') {
bindedFunction = _.bind(fetchFunction, this, path);
} else {
bindedFunction = _.bind(customFunction, this, operation, path);
}
var createdFunction = function(params, headers, elem) {
var callParams = _.defaults({
params: params,
headers: headers,
elem: elem
}, {
params: defaultParams,
headers: defaultHeaders,
elem: defaultElem
});
return bindedFunction(callParams.params, callParams.headers, callParams.elem);
};
if (config.isSafe(operation)) {
this[name] = createdFunction;
} else {
this[name] = function(elem, params, headers) {
return createdFunction(params, headers, elem);
};
}
}
function withConfigurationFunction(configurer) {
var newConfig = angular.copy(_.omit(config, 'configuration'));
Configurer.init(newConfig, newConfig);
configurer(newConfig);
return createServiceForConfiguration(newConfig);
}
function toService(route, parent) {
var knownCollectionMethods = _.values(config.restangularFields);
var serv = {};
var collection = (parent || service).all(route);
serv.one = _.bind(one, (parent || service), parent, route);
serv.post = _.bind(collection.post, collection);
serv.getList = _.bind(collection.getList, collection);
serv.withHttpConfig = _.bind(collection.withHttpConfig, collection);
serv.get = _.bind(collection.get, collection);
for (var prop in collection) {
if (collection.hasOwnProperty(prop) && _.isFunction(collection[prop]) && !_.includes(knownCollectionMethods, prop)) {
serv[prop] = _.bind(collection[prop], collection);
}
}
return serv;
}
Configurer.init(service, config);
service.copy = _.bind(copyRestangularizedElement, service);
service.service = _.bind(toService, service);
service.withConfig = _.bind(withConfigurationFunction, service);
service.one = _.bind(one, service, null);
service.all = _.bind(all, service, null);
service.several = _.bind(several, service, null);
service.oneUrl = _.bind(oneUrl, service, null);
service.allUrl = _.bind(allUrl, service, null);
service.stripRestangular = _.bind(stripRestangular, service);
service.restangularizeElement = _.bind(restangularizeElem, service);
service.restangularizeCollection = _.bind(restangularizeCollectionAndElements, service);
return service;
}
return createServiceForConfiguration(globalConfiguration);
}];
});
return restangular.name;
}));
================================================
FILE: test/restangularSpec.js
================================================
/* global describe, beforeEach, afterEach, it, expect, spyOn, jasmine */
/* jshint unused: false */
describe('Restangular', function () {
// API
var Restangular, $httpBackend, testData,
restangularAccounts, restangularAccount0, restangularAccount1;
// Load required modules
beforeEach(function () {
// Load restangular module
angular.mock.module('restangular');
// Get references to modules from the injector
angular.mock.inject(function ($injector) {
$httpBackend = $injector.get('$httpBackend');
Restangular = $injector.get('Restangular');
});
// Restangularize a few demo accounts
restangularAccounts = Restangular.all('accounts');
restangularAccount0 = Restangular.one('accounts', 0);
restangularAccount1 = Restangular.one('accounts', 1);
// Create testdata for our tests
testData = {
// Model
accountsModel: [{
id: 0,
user: 'Martin ',
amount: 42,
transactions: []
}, {
id: 1,
user: 'Paul',
amount: 3.1416,
transactions: [{
from: 'Martin',
amount: 3,
id: 0
}, {
from: 'Anonymous',
amount: 0.1416,
id: 1
}]
}],
nextAccountId: 22,
// HAL model (http://stateless.co/hal_specification.html)
accountsHalModel: [{
id: 0,
user: 'Martin',
amount: 42,
transaction: [],
_links: {
self: '/accountsHAL/martin'
}
}, {
id: 1,
user: 'Paul',
amount: 3.1416,
transaction: [{
from: 'Martin',
amount: 3,
id: 0,
_links: {
self: '/accountsHAL/paul/transactions/0'
}
}, {
from: 'Anonymous',
amount: 0.1416,
id: 1,
_links: {
self: '/accountsHAL/paul/transactions/1'
}
}],
_links: {
self: '/accountsHAL/paul'
}
}],
infoModel: {
id: 0,
text: 'Some additional account information'
},
newAccount: {
user: 'First User',
amount: 45,
transactions: []
},
messages: [{
id: 23,
name: 'Gonto'
}, {
id: 45,
name: 'John'
}],
accountsDoSomethingModel: {
result: 1
},
// Another API for testing
customers: [{
id: 0,
name: 'Alice',
status: 'active',
credit: 4000.0
}, {
id: 1,
name: 'Bob',
status: 'active',
credit: 4000.0
}, {
id: 2,
name: 'Carl',
status: 'active',
credit: 4000.0
}],
publications: [{
id: 1,
title: 'Sample',
content: 'Rich data',
tags: [
'science',
'chemistry'
]
}],
newCustomer: {
id: 3,
name: 'New',
status: 'active',
credit: 4000.0
}
};
// Set up backend responses
$httpBackend.when('HEAD', '/accounts').respond();
$httpBackend.when('TRACE', '/accounts').respond();
$httpBackend.when('OPTIONS', '/accounts').respond();
// CRUD /accounts
$httpBackend.whenGET('/accounts').respond(testData.accountsModel);
$httpBackend.whenJSONP('/accounts').respond(testData.accountsModel);
$httpBackend.whenPOST('/accounts').respond(function (method, url, data, headers) {
var newData = angular.fromJson(data);
newData.fromServer = true;
newData.id = testData.nextAccountId;
return [201, JSON.stringify(newData), ''];
});
$httpBackend.whenGET('/accounts/do-something').respond(testData.accountsDoSomethingModel);
$httpBackend.whenGET('/accounts/search/byOwner').respond(testData.accountsModel);
// CRUD /accounts/{id}
$httpBackend.whenGET('/accounts/0,1').respond(testData.accountsModel);
$httpBackend.whenGET('/accounts/messages').respond(testData.messages);
$httpBackend.whenGET('/accounts/1/message').respond(testData.messages[0]);
$httpBackend.whenGET('/accounts/1/messages').respond(testData.messages);
$httpBackend.whenGET('/accounts/0').respond(testData.accountsModel[0]);
$httpBackend.whenGET('/accounts/1').respond(testData.accountsModel[1]);
$httpBackend.whenJSONP('/accounts/1').respond(testData.accountsModel[1]);
$httpBackend.whenGET('/accounts/1/transactions').respond(testData.accountsModel[1].transactions);
$httpBackend.whenGET('/accounts/1/transactions/1').respond(testData.accountsModel[1].transactions[1]);
$httpBackend.whenPOST('/accounts/1/transactions').respond(function (method, url, data, headers) {
return [201, '', ''];
});
$httpBackend.whenDELETE('/accounts/1/transactions/1').respond(function (method, url, data, headers) {
return [200, '', ''];
});
$httpBackend.whenDELETE('/accounts/1').respond(function (method, url, data, headers) {
return [200, '', ''];
});
$httpBackend.whenPOST('/accounts/1').respond(function (method, url, data, headers) {
return [200, '', ''];
});
$httpBackend.whenPUT('/accounts/1').respond(function (method, url, data, headers) {
testData.accountsModel[1] = angular.fromJson(data);
return [201, data, ''];
});
$httpBackend.whenGET('/info').respond(testData.infoModel);
$httpBackend.whenGET('/accounts/1/info').respond(testData.infoModel);
$httpBackend.whenPUT('/info').respond(function (method, url, data) {
return [200, data, ''];
});
$httpBackend.whenGET('/accountsHAL').respond(testData.accountsHalModel);
$httpBackend.whenPUT('/accountsHAL/martin').respond(function (method, url, data) {
testData.accountsHalModel[0] = angular.fromJson(data);
return [200, data, ''];
});
// Full URL
$httpBackend.whenGET('http://accounts.com/all').respond(testData.accountsModel);
$httpBackend.whenGET('/error').respond(function () {
return [500, {}, ''];
});
$httpBackend.whenGET('/misc/zero').respond(function () {
return [200, 0, ''];
});
// return the status code given
// e.g.: /error/404 returns 404 Not Found
var urlRegex = /\/error\/(\d{3})/;
$httpBackend.whenGET(urlRegex).respond(function (method, url, data, headers) {
return [url.match(urlRegex)[1], {}, ''];
});
$httpBackend.whenGET('/customers/').respond(testData.customers);
$httpBackend.whenGET('http://localhost:8080/customers/').respond(testData.customers);
$httpBackend.whenGET('api.new.domain/customers/').respond(testData.customers);
$httpBackend.whenGET('/customers/?active=true').respond(testData.customers);
$httpBackend.whenGET('/customers/publications/?tags=chemistry').respond(testData.publications);
$httpBackend.whenPUT('/customers/0').respond(function (method, url, data) {
testData.customers[0] = angular.fromJson(data);
return [200, data, ''];
});
$httpBackend.whenPOST('/customers/').respond(function (method, url, data, headers) {
var newData = angular.fromJson(data);
newData.fromServer = true;
return [201, JSON.stringify(newData), ''];
});
}); // END OF BEFOREEACH
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
describe('stripRestangular', function () {
// We test stripRestangular by saving objects and checking
// the data received by the backend in the POST request.
// StripRestangular is used to remove Restangular's methods
// from the restangularized POST data (element), but not from raw data.
it('should not strip Restangular properties from raw POST data', function () {
// https://github.com/mgonto/restangular/issues/374
var restangularFields = Restangular.configuration.restangularFields;
// create an object whose keys are the values of the restangularFields
var postData = {};
_.each(restangularFields, function (value, key) {
postData.value = value;
});
// we don't want our post data to be treated as a restangularized object
postData.restangularized = false;
// when posting, restangular shouldn't remove any of our properties
var expectedData = angular.copy(postData);
$httpBackend.expectPOST('/accounts/1/merge', expectedData).respond(200);
var parent = Restangular.restangularizeElement(null, {id: 1}, 'accounts', true);
parent.post('merge', postData);
$httpBackend.flush();
});
it('should not strip "original" Restangular properties in restangularized POST data when overriding restangularFields', function () {
// https://github.com/mgonto/restangular/issues/374
// Here, we want to post a data object with fields
// that normally are used by Restangular, such that save, ids, options etc.
// We do that by taking each field in restangularFields and overriding it
// with something else, making restangular use different properties
// for its internal properties and functions, freeing the original ones
// for use in our data object
// these are the original field names used by restangular
var restangularFields = Restangular.configuration.restangularFields;
// create an object whose keys are the values of the restangularFields
// i.e. {save: "save", clone: "clone", doPOST: "doPOST", ...}
var postData = {};
_.each(restangularFields, function (value, key) {
postData.value = value;
});
// we expect the http service to get all of these "original" properties in the data object
var expectedData = angular.copy(postData);
// Override the field names used internally by Restangular,
// the new config will be something like
// {id: '_id', save: '_save', clone: '_clone', ...}
var newFieldConfig = {};
_.each(restangularFields, function (value, key) {
newFieldConfig[key] = '_' + key;
});
Restangular.setRestangularFields(newFieldConfig);
// Restangularize the data as an element to save
var parent = Restangular.restangularizeElement(null, postData, 'accounts', false);
// make the POST and check the posted data
$httpBackend.expectPOST('/accounts', expectedData).respond(200);
parent._save(); // we've overriden the save method as _save
$httpBackend.flush();
});
});
describe('Interceptors', function () {
it('Should add multiple request and response interceptors', function () {
Restangular.addRequestInterceptor(function (elem) {
var elemCopy = angular.copy(elem);
elemCopy.firstRequestInterceptor = true;
return elemCopy;
});
Restangular.addRequestInterceptor(function (elem) {
expect(elem.firstRequestInterceptor).toBeDefined();
var elemCopy = angular.copy(elem);
elemCopy.secondRequestInterceptor = true;
return elemCopy;
});
Restangular.addFullRequestInterceptor(function (elem) {
expect(elem.firstRequestInterceptor).toBeDefined();
expect(elem.secondRequestInterceptor).toBeDefined();
var elemCopy = angular.copy(elem);
elemCopy.thirdRequestInterceptor = true;
return {
element: elemCopy
};
});
Restangular.addResponseInterceptor(function (elem) {
var elemCopy = angular.copy(elem);
elemCopy.firstResponseInterceptor = true;
return elemCopy;
});
Restangular.addResponseInterceptor(function (elem) {
expect(elem.firstResponseInterceptor).toBeDefined();
var elemCopy = angular.copy(elem);
elemCopy.secondResponseInterceptor = true;
return elemCopy;
});
$httpBackend.whenPOST('/list').respond(function (method, url, data, headers) {
var elem = angular.fromJson(data);
expect(elem.firstRequestInterceptor).toBeDefined();
expect(elem.secondRequestInterceptor).toBeDefined();
expect(elem.thirdRequestInterceptor).toBeDefined();
return [200, elem, ''];
});
$httpBackend.expectPOST('/list');
Restangular.all('list').post({
name: 'Gonto'
}).then(function (elem) {
expect(elem.firstResponseInterceptor).toBeDefined();
expect(elem.secondResponseInterceptor).toBeDefined();
});
$httpBackend.flush();
});
it('Should add multiple error interceptors', function () {
$httpBackend.expectGET('/error');
var CallbackManager = function () {};
CallbackManager.successCallback = function () {
expect(CallbackManager.successCallback).not.toHaveBeenCalled();
};
CallbackManager.errorCallback = function () {
expect(CallbackManager.firstErrorInterceptor).toHaveBeenCalled();
expect(CallbackManager.secondErrorInterceptor).toHaveBeenCalled();
};
CallbackManager.firstErrorInterceptor = function () {};
CallbackManager.secondErrorInterceptor = function () {};
spyOn(CallbackManager, 'successCallback').and.callThrough();
spyOn(CallbackManager, 'firstErrorInterceptor').and.callThrough();
spyOn(CallbackManager, 'secondErrorInterceptor').and.callThrough();
Restangular.addErrorInterceptor(CallbackManager.firstErrorInterceptor);
Restangular.addErrorInterceptor(CallbackManager.secondErrorInterceptor);
Restangular.all('error').getList()
.then(CallbackManager.successCallback)
.catch(CallbackManager.errorCallback);
$httpBackend.flush();
});
it('Should add multiple error interceptors but don\'t reject the promise if one of them returns false', function () {
$httpBackend.expectGET('/error');
var CallbackManager = function () {};
CallbackManager.successCallback = function () {
expect(CallbackManager.successCallback).not.toHaveBeenCalled();
};
CallbackManager.errorCallback = function () {
expect(CallbackManager.errorCallback).not.toHaveBeenCalled();
};
CallbackManager.firstErrorInterceptor = function () {
return true;
};
CallbackManager.secondErrorInterceptor = function () {
return false; // prevent promise to be rejected
};
spyOn(CallbackManager, 'successCallback').and.callThrough();
spyOn(CallbackManager, 'errorCallback').and.callThrough();
Restangular.addErrorInterceptor(CallbackManager.firstErrorInterceptor);
Restangular.addErrorInterceptor(CallbackManager.secondErrorInterceptor);
Restangular.all('error').getList()
.then(CallbackManager.successCallback)
.catch(CallbackManager.errorCallback);
$httpBackend.flush();
});
it('Should add multiple error interceptors for a single get too', function () {
$httpBackend.expectGET('/error/404');
var CallbackManager = function () {};
CallbackManager.successCallback = function () {
expect(CallbackManager.successCallback).not.toHaveBeenCalled();
};
CallbackManager.errorCallback = function () {
expect(CallbackManager.firstErrorInterceptor).toHaveBeenCalled();
expect(CallbackManager.secondErrorInterceptor).toHaveBeenCalled();
};
CallbackManager.firstErrorInterceptor = function (response) {
expect(Number(response.status)).toEqual(404);
};
CallbackManager.secondErrorInterceptor = function () {};
spyOn(CallbackManager, 'successCallback').and.callThrough();
spyOn(CallbackManager, 'firstErrorInterceptor').and.callThrough();
spyOn(CallbackManager, 'secondErrorInterceptor').and.callThrough();
Restangular.addErrorInterceptor(CallbackManager.firstErrorInterceptor);
Restangular.addErrorInterceptor(CallbackManager.secondErrorInterceptor);
Restangular.one('error', 404).get()
.then(CallbackManager.successCallback)
.catch(CallbackManager.errorCallback);
$httpBackend.flush();
});
});
describe('Transformers', function () {
it('Should decorate element both on server and local by default', function () {
Restangular.extendModel('accounts', function (account) {
account.extended = function () {
return true;
};
return account;
});
Restangular.one('accounts', 1).get().then(function (account) {
expect(account.extended).toBeDefined();
});
var local = {};
Restangular.restangularizeElement(null, local, 'accounts');
expect(local.extended).toBeDefined();
$httpBackend.flush();
});
});
describe('With Suffix', function () {
it('shouldn\'t add suffix to getRestangularUrl', function () {
var suffixRestangular = Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setRequestSuffix('.json');
});
var collection = suffixRestangular.all('accounts');
expect(collection.getRestangularUrl()).toBe('/accounts');
expect(collection.one('1').getRestangularUrl()).toBe('/accounts/1');
});
it('should add suffix to getRequestedUrl', function () {
var suffixRestangular = Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setRequestSuffix('.json');
});
var collection = suffixRestangular.all('accounts');
expect(collection.getRequestedUrl()).toBe('/accounts.json');
expect(collection.one('1').getRequestedUrl()).toBe('/accounts/1.json');
});
it('should add suffix to request', function () {
var suffixRestangular = Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setRequestSuffix('.json');
});
var collection = suffixRestangular.all('accounts');
$httpBackend.expectGET('/accounts.json').respond(200);
$httpBackend.expectGET('/accounts/1.json').respond(200);
collection.getList();
collection.get('1');
$httpBackend.flush();
});
it('shouldn\'t add suffix to allUrl', function () {
var suffixRestangular = Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setRequestSuffix('.json');
});
$httpBackend.expectGET('http://accounts.com/all');
suffixRestangular.allUrl('accounts', 'http://accounts.com/all').getList();
$httpBackend.flush();
});
});
describe('JSONp', function () {
it('should work for get', function () {
Restangular.setJsonp(true);
Restangular.one('accounts', 1).get();
$httpBackend.expectJSONP('/accounts/1');
$httpBackend.flush();
});
it('should work for getList', function () {
Restangular.setJsonp(true);
Restangular.all('accounts').getList();
$httpBackend.expectJSONP('/accounts');
$httpBackend.flush();
});
it('shouldn\'t override post', function () {
Restangular.setJsonp(true);
restangularAccounts.post({
id: 2,
user: 'Someone'
});
$httpBackend.expectPOST('/accounts').respond(201, '');
$httpBackend.flush();
});
});
describe('Local data', function () {
it('Should restangularize a collection OK', function () {
var collection = angular.copy(testData.accountsModel);
Restangular.restangularizeCollection(null, collection, 'accounts');
expect(_.has(collection, 'get')).toBe(true);
expect(_.has(collection[0], 'get')).toBe(true);
expect(collection.getRestangularUrl()).toBe('/accounts');
expect(collection[0].getRestangularUrl()).toBe('/accounts/0');
});
it('Should restangularize a function with arguments OK', function () {
var collection = function (a, b) {};
Restangular.restangularizeCollection(null, collection, 'accounts');
expect(_.has(collection, 'get')).toBe(true);
expect(collection.getRestangularUrl()).toBe('/accounts');
});
it('should have fromServer set when restangularizeElement is called with that param', function () {
var element = Restangular.restangularizeElement(null, {}, 'accounts', true);
expect(element.fromServer).toEqual(true);
element = Restangular.restangularizeElement(null, {}, 'accounts', false);
expect(element.fromServer).toEqual(false);
element = Restangular.restangularizeElement(null, {}, 'accounts');
expect(element.fromServer).toEqual(false);
});
it('should have fromServer set when restangularizeCollection is called with that param', function () {
var collection = Restangular.restangularizeCollection(null, [{}], 'accounts', true);
expect(collection[0].fromServer).toEqual(true);
collection = Restangular.restangularizeCollection(null, [{}], 'accounts', false);
expect(collection[0].fromServer).toEqual(false);
collection = Restangular.restangularizeCollection(null, [{}], 'accounts');
expect(collection[0].fromServer).toEqual(false);
});
});
describe('restangularizePromiseIntercept', function () {
it('should be invoked by restangularizePromise', function () {
var calledWithPromise;
Restangular.setRestangularizePromiseInterceptor(function (promise) {
calledWithPromise = promise;
promise.$object.$custom = true;
});
var promise = Restangular.one('accounts', 1).get();
expect(calledWithPromise).toBeDefined();
expect(promise.$object.$custom).toBeDefined();
$httpBackend.flush();
});
});
describe('$object', function () {
it('Should work for single get', function () {
var promise = Restangular.one('accounts', 1).get();
var obj = promise.$object;
expect(obj).toBeDefined();
expect(obj.amount).toBeUndefined();
$httpBackend.flush();
expect(obj.amount).toEqual(3.1416);
});
it('Shouldn\'t be restangularized by default', function () {
Restangular.extendModel('accounts', function (account) {
account.extended = function () {
return true;
};
return account;
});
var promise = Restangular.one('accounts', 1).get();
var obj = promise.$object;
expect(obj).toBeDefined();
expect(obj.extended).toBeUndefined();
$httpBackend.flush();
});
it('Should work for single get', function () {
var promise = Restangular.all('accounts').getList();
var list = promise.$object;
expect(list).toBeDefined();
expect(list.length).toEqual(0);
$httpBackend.flush();
expect(list.length).toEqual(2);
expect(list[1].amount).toEqual(3.1416);
});
});
describe('ALL', function () {
it('getList() should return an array of items', function () {
restangularAccounts.getList().then(function (accounts) {
expect(Restangular.stripRestangular(accounts)).toEqual(Restangular.stripRestangular(testData.accountsModel));
});
$httpBackend.flush();
});
it('several getList() should return an array of items', function () {
$httpBackend.expectGET('/accounts/0,1');
Restangular.several('accounts', 0, 1).getList().then(function (accounts) {
expect(Restangular.stripRestangular(accounts)).toEqual(Restangular.stripRestangular(testData.accountsModel));
});
$httpBackend.flush();
});
it('several remove() should work', function () {
$httpBackend.expectDELETE('/accounts/0,1').respond([200, '', '']);
Restangular.several('accounts', 0, 1).remove();
$httpBackend.flush();
});
it('get(id) should return the item with given id', function () {
restangularAccounts.get(0).then(function (account) {
expect(Restangular.stripRestangular(account)).toEqual(Restangular.stripRestangular(testData.accountsModel[0]));
});
$httpBackend.flush();
});
it('uses all to get the list without parameters', function () {
Restangular.one('accounts', 1).all('messages').getList();
$httpBackend.expectGET('/accounts/1/messages');
$httpBackend.flush();
});
it('Custom GET methods should work', function () {
restangularAccounts.customGETLIST('messages').then(function (msgs) {
expect(Restangular.stripRestangular(msgs)).toEqual(Restangular.stripRestangular(testData.messages));
});
$httpBackend.flush();
});
it('post() should add a new item', function () {
restangularAccounts.post({
id: 2,
user: 'Someone'
}).then(function () {
expect(testData.accountsModel.length).toEqual(2);
});
$httpBackend.expectPOST('/accounts').respond(201, '');
$httpBackend.flush();
});
it('customPOST() should add a new item', function () {
restangularAccounts.customPOST({
id: 2,
user: 'Someone'
}).then(function () {
expect(testData.accountsModel.length).toEqual(2);
});
$httpBackend.expectPOST('/accounts').respond(201, '');
$httpBackend.flush();
});
it('post() should work with arrays', function () {
Restangular.all('places').post([{
name: 'Gonto'
}, {
name: 'John'
}]).then(function (value) {
expect(value.length).toEqual(2);
});
$httpBackend.expectPOST('/places').respond(function (method, url, data, headers) {
return [201, angular.fromJson(data), ''];
});
$httpBackend.flush();
});
it('post() should add a new item with data and return the data from the server', function () {
restangularAccounts.post(testData.newAccount).then(function (added) {
expect(added.fromServer).toEqual(true);
expect(added.id).toEqual(testData.nextAccountId);
expect(added.user).toEqual(testData.newAccount.user);
});
$httpBackend.expectPOST('/accounts');
$httpBackend.flush();
});
it('Doing a post and then other operation (delete) should call right URLs', function () {
restangularAccounts.post(testData.newAccount).then(function (added) {
added.remove();
$httpBackend.expectDELETE('/accounts/' + testData.nextAccountId).respond(201, '');
});
$httpBackend.flush();
});
it('Doing a post to a server that returns no element will return undefined', function () {
restangularAccounts.getList().then(function (accounts) {
var newTransaction = {
id: 1,
name: 'Gonto'
};
accounts[1].post('transactions', newTransaction).then(function (transaction) {
expect(transaction).toBeUndefined();
});
});
$httpBackend.flush();
});
it('head() should safely return', function () {
restangularAccounts.head().then(function () {
expect(true).toBe(true);
});
$httpBackend.flush();
});
it('trace() should safely return', function () {
restangularAccounts.trace().then(function () {
expect(true).toBe(true);
});
$httpBackend.flush();
});
it('customPUT should work', function () {
$httpBackend.expectPUT('/accounts/hey').respond(testData.accountsModel);
restangularAccounts.customPUT({
key: 'value'
}, 'hey');
$httpBackend.flush();
});
it('customPATCH should work', function () {
var data = {
foo: 'bar'
};
$httpBackend.expectPATCH('/accounts/hey', data).respond(testData.accountsModel);
restangularAccounts.customPATCH(data, 'hey');
$httpBackend.flush();
});
it('options() should safely return', function () {
restangularAccounts.options().then(function () {
expect(true).toBe(true);
});
$httpBackend.flush();
});
it('getList() should correctly handle params after customDELETE', function () {
$httpBackend.expectGET('/accounts?foo=1').respond(testData.accountsModel);
restangularAccounts.getList({
foo: 1
}).then(function () {
$httpBackend.expectDELETE('/accounts?id=1').respond(201, '');
return restangularAccounts.customDELETE('', {
id: 1
});
}).then(function () {
$httpBackend.expectGET('/accounts?foo=1').respond(testData.accountsModel);
return restangularAccounts.getList({
foo: 1
});
}).then(function (accounts) {
expect(Restangular.stripRestangular(accounts)).toEqual(Restangular.stripRestangular(testData.accountsModel));
});
$httpBackend.flush();
});
});
describe('Scoped Service', function () {
it('should correctly work', function () {
var Accounts = Restangular.service('accounts');
Accounts.post(testData.newAccount);
Accounts.one(0).get();
Accounts.getList();
$httpBackend.expectPOST('/accounts');
$httpBackend.expectGET('/accounts/0');
$httpBackend.expectGET('/accounts');
$httpBackend.flush();
});
it('should correctly work with children', function () {
var Transactions = Restangular.service('transactions', restangularAccount1);
Transactions.post(testData.newAccount);
Transactions.one(1).get();
Transactions.getList();
Transactions.get(1);
$httpBackend.expectPOST('/accounts/1/transactions');
$httpBackend.expectGET('/accounts/1/transactions/1');
$httpBackend.expectGET('/accounts/1/transactions');
$httpBackend.expectGET('/accounts/1/transactions/1');
$httpBackend.flush();
});
it('should add custom collection method added with withConfig', function () {
var Accounts = Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.addElementTransformer('accounts', true, function (worker) {
worker.addRestangularMethod('doSomething', 'get', 'do-something');
return worker;
});
}).service('accounts');
expect(Accounts.doSomething).toBeDefined();
expect(_.isFunction(Accounts.doSomething)).toBeTruthy();
Accounts.post(testData.newAccount);
Accounts.one(0).get();
Accounts.getList();
Accounts.doSomething();
Accounts.get(0);
$httpBackend.expectPOST('/accounts');
$httpBackend.expectGET('/accounts/0');
$httpBackend.expectGET('/accounts');
$httpBackend.expectGET('/accounts/do-something');
$httpBackend.expectGET('/accounts/0');
$httpBackend.flush();
});
it('should provide a one-off $http configuration method', function () {
var Accounts = Restangular.service('accounts');
Accounts.withHttpConfig({
transformRequest: angular.identity
});
Accounts.post(testData.newAccount);
$httpBackend.expectPOST('/accounts');
$httpBackend.flush();
});
});
describe('ONE', function () {
it('get() should return a JSON item', function () {
restangularAccount1.get().then(function (account) {
expect(Restangular.stripRestangular(account))
.toEqual(Restangular.stripRestangular(testData.accountsModel[1]));
});
$httpBackend.flush();
});
it('Should save as put correctly', function () {
restangularAccount1.get().then(function (account) {
$httpBackend.expectPUT('/accounts/1');
account.put();
});
$httpBackend.flush();
});
it('Should save as post correctly', function () {
var account1 = angular.copy(restangularAccount1);
$httpBackend.expectPOST('/accounts/1');
account1.name = 'Hey';
account1.save();
$httpBackend.flush();
});
it('Should keep route property when element is created', function () {
var account1 = Restangular.restangularizeElement(null, {}, 'accounts');
$httpBackend.expectPOST('/accounts');
$httpBackend.expectPUT('/accounts/1');
account1.name = 'Hey';
account1.save().then(function (accountFromServer) {
accountFromServer.id = 1;
return accountFromServer.save();
}).then(function (accountFromServer2) {
expect(accountFromServer2.route).toBe(account1.route);
});
$httpBackend.flush();
});
it('Should make RequestLess connections with one', function () {
restangularAccount1.one('transactions', 1).get().then(function (transaction) {
expect(Restangular.stripRestangular(transaction))
.toEqual(Restangular.stripRestangular(testData.accountsModel[1].transactions[1]));
});
$httpBackend.flush();
});
it('Should make RequestLess connections with all', function () {
restangularAccount1.all('transactions').getList().then(function (transactions) {
expect(Restangular.stripRestangular(transactions))
.toEqual(Restangular.stripRestangular(testData.accountsModel[1].transactions));
});
$httpBackend.flush();
});
it('Custom GET methods should work', function () {
restangularAccount1.customGET('message').then(function (msg) {
expect(Restangular.stripRestangular(msg)).toEqual(Restangular.stripRestangular(testData.messages[0]));
});
$httpBackend.flush();
});
it('put() should update the value', function () {
restangularAccount1.get().then(function (account) {
account.amount = 1.618;
account.put().then(function (newAc) {
expect(testData.accountsModel[1].amount).toEqual(1.618);
newAc.remove();
$httpBackend.expectDELETE('/accounts/1');
});
$httpBackend.expectPUT('/accounts/1');
});
$httpBackend.flush();
});
it('should return an array when accessing a subvalue', function () {
restangularAccount1.get().then(function (account) {
account.getList('transactions').then(function (transactions) {
expect(Restangular.stripRestangular(transactions))
.toEqual(Restangular.stripRestangular(testData.accountsModel[1].transactions));
});
});
$httpBackend.flush();
});
});
describe('COPY', function () {
it('should copy an object and "this" should reference the copied object', function () {
var copiedAccount = Restangular.copy(testData.accountsModel[0]);
var that;
copiedAccount.user = 'Copied string';
expect(copiedAccount).not.toBe(testData.accountsModel[0]);
// create a spy for one of the methods to capture the value of 'this'
spyOn(copiedAccount, 'getRestangularUrl').and.callFake(function () {
that = this;
});
copiedAccount.getRestangularUrl(); // invoke the method we are spying on
expect(that).toBe(copiedAccount);
});
it('should copy an object and "fromServer" param should be the same with the copied object', function () {
var responseHandler = jasmine.createSpy();
// with fromServer=true
restangularAccount1.get().then(responseHandler);
$httpBackend.flush();
var account = responseHandler.calls.argsFor(0)[0],
copiedAccount = Restangular.copy(account);
expect(account.fromServer).toEqual(true);
expect(copiedAccount.fromServer).toEqual(true);
// with fromServer=false
account = Restangular.one('accounts', 123),
copiedAccount = Restangular.copy(account);
expect(account.fromServer).toEqual(false);
expect(copiedAccount.fromServer).toEqual(false);
});
it('should copy a collection and "fromServer" param should stay the same', function () {
var responseHandler = jasmine.createSpy();
// with collections, fromServer=false
var accounts = Restangular.all('accounts'),
copiedAccounts = Restangular.copy(accounts);
expect(accounts.fromServer).toEqual(false);
expect(copiedAccounts.fromServer).toEqual(false);
// with collections, fromServer = true;
restangularAccounts.getList().then(responseHandler);
$httpBackend.flush();
accounts = responseHandler.calls.argsFor(0)[0],
copiedAccounts = Restangular.copy(accounts);
expect(accounts.fromServer).toEqual(true);
expect(copiedAccounts.fromServer).toEqual(true);
});
it('should copy an object and "route" param should be the same in the copied object', function () {
// for element
var account = Restangular.one('accounts', 123),
copiedAccount = Restangular.copy(account);
expect(account.route).toEqual(copiedAccount.route);
// for collection
var accounts = Restangular.all('accounts'),
copiedAccounts = Restangular.copy(accounts);
expect(accounts.route).toEqual(copiedAccounts.route);
});
it('should copy an object and the parent property should stay the same', function () {
// element
var user = Restangular.one('account', 12).one('user', 14),
userCopy = Restangular.copy(user);
expect(user.parentResource.route).toEqual('account');
expect(userCopy.parentResource.route).toEqual('account');
// collection
var users = Restangular.one('account', 12).all('users'),
usersCopy = Restangular.copy(users);
expect(user.parentResource.route).toEqual('account');
expect(usersCopy.parentResource.route).toEqual('account');
});
});
describe('getRestangularUrl', function () {
it('should get the URL for the current object', function () {
var element = Restangular.one('accounts', 123);
expect(element.getRestangularUrl()).toEqual('/accounts/123');
});
it('should not include query parameters', function () {
var responseHandler = jasmine.createSpy(), element;
$httpBackend.expectGET('/accounts/123?query=params').respond({id: 123, name: 'account123'});
Restangular.one('accounts', 123).get({query: 'params'}).then(responseHandler);
$httpBackend.flush();
element = responseHandler.calls.argsFor(0)[0];
expect(element.getRestangularUrl()).toEqual('/accounts/123');
});
it('should be the same for the built resource as for the fetched resource', function () {
var responseHandler = jasmine.createSpy(), element, resource;
$httpBackend.expectGET('/accounts/123').respond({id: 123, name: 'Account 123'});
resource = Restangular.one('accounts', 123);
resource.get().then(responseHandler);
$httpBackend.flush();
element = responseHandler.calls.argsFor(0)[0];
expect(resource.getRestangularUrl()).toEqual('/accounts/123');
expect(element.getRestangularUrl()).toEqual('/accounts/123');
});
it('should use the id from the response, not the request', function () {
var responseHandler = jasmine.createSpy(), element, resource;
$httpBackend.expectGET('/accounts/123').respond({id: 444, name: 'Account 444'});
resource = Restangular.one('accounts', 123);
resource.get().then(responseHandler);
$httpBackend.flush();
element = responseHandler.calls.argsFor(0)[0];
expect(resource.getRestangularUrl()).toEqual('/accounts/123');
expect(element.getRestangularUrl()).toEqual('/accounts/444');
});
it('should have an empty id in the URL if the response id is empty', function () {
// https://github.com/mgonto/restangular/issues/1421
var responseHandler = jasmine.createSpy(), element, resource;
$httpBackend.expectGET('/accounts/123').respond({name: 'Account 444'});
resource = Restangular.one('accounts', 123);
resource.get().then(responseHandler);
$httpBackend.flush();
element = responseHandler.calls.argsFor(0)[0];
expect(resource.getRestangularUrl()).toEqual('/accounts/123');
expect(element.getRestangularUrl()).toEqual('/accounts');
});
it('should return the generated URL for PUTed elements', function () {
var responseHandler = jasmine.createSpy(), element;
$httpBackend.expectPUT('/accounts/123').respond({id: 123, name: 'Account 123'});
Restangular.one('accounts', 123).put().then(responseHandler);
$httpBackend.flush();
element = responseHandler.calls.argsFor(0)[0];
expect(element.getRestangularUrl()).toEqual('/accounts/123');
});
it('should return the generated URL for POSTed elements', function () {
var responseHandler = jasmine.createSpy(), element;
$httpBackend.expectPOST('/accounts').respond({id: 123, name: 'Account 123'});
Restangular.restangularizeElement(null, {name: 'Account 123'}, 'accounts', false, false).save().then(responseHandler);
$httpBackend.flush();
element = responseHandler.calls.argsFor(0)[0];
expect(element.getRestangularUrl()).toEqual('/accounts/123');
});
it('should return the generated URL when you chain Restangular methods together', function () {
var restangularSpaces = Restangular.one('accounts', 123).one('buildings', 456).all('spaces');
expect(restangularSpaces.getRestangularUrl()).toEqual('/accounts/123/buildings/456/spaces');
});
describe('with useCannonicalId set to true', function () {
it('should return the generated URL when you chain Restangular methods together', function () {
var R = Restangular.withConfig(function (config) {
config.setUseCannonicalId(true);
});
var restangularSpaces = R.one('accounts', 123).one('buildings', 456).all('spaces');
expect(restangularSpaces.getRestangularUrl()).toEqual('/accounts/123/buildings/456/spaces');
});
});
});
describe('addElementTransformer', function () {
it('should allow for a custom method to be placed at the collection level', function () {
var accountsPromise;
Restangular.addElementTransformer('accounts', true, function (collection) {
collection.totalAmount = function () {};
return collection;
});
accountsPromise = Restangular.all('accounts').getList();
accountsPromise.then(function (accounts) {
expect(typeof accounts.totalAmount).toEqual('function');
});
$httpBackend.flush();
});
it('should allow for a custom method to be placed at the model level when one model is requested', function () {
var accountPromise;
Restangular.addElementTransformer('accounts', false, function (model) {
model.prettifyAmount = function () {};
return model;
});
accountPromise = Restangular.one('accounts', 1).get();
accountPromise.then(function (account) {
expect(typeof account.prettifyAmount).toEqual('function');
});
$httpBackend.flush();
});
it('should allow for a custom method to be placed at the model level when several models are requested', function () {
var accountsPromise;
Restangular.addElementTransformer('accounts', false, function (model) {
model.prettifyAmount = function () {};
return model;
});
accountsPromise = Restangular.all('accounts', 1).getList();
accountsPromise.then(function (accounts) {
accounts.forEach(function (account) {
expect(typeof account.prettifyAmount).toEqual('function');
});
});
$httpBackend.flush();
});
it('should allow for a custom method to be placed at the collection level using a regexp matching the route', function () {
var accountsPromise;
Restangular.addElementTransformer(/^accounts/, false, function (model) {
model.prettifyAmount = function () {};
return model;
});
accountsPromise = Restangular.all('accounts/search/byOwner', 1).getList();
accountsPromise.then(function (accounts) {
accounts.forEach(function (account) {
expect(typeof account.prettifyAmount).toEqual('function');
});
});
$httpBackend.flush();
});
it('should work with cloned collections', function () {
var responseHandler = jasmine.createSpy();
Restangular.addElementTransformer(/^accounts/, true, function (collection) {
collection.customThing = 'customValue';
return collection;
});
Restangular.all('accounts').getList().then(responseHandler);
$httpBackend.flush();
var accounts = responseHandler.calls.argsFor(0)[0];
var accountsCopy = accounts.clone();
expect(accounts.customThing).toEqual('customValue');
expect(accountsCopy.customThing).toEqual('customValue');
});
it('should allow for a custom method to be placed at the model level using regexp route when one model is requested', function () {
var accountPromise;
Restangular.addElementTransformer(/^accounts/, false, function (model) {
model.prettifyAmount = function () {};
return model;
});
accountPromise = Restangular.one('accounts', 1).get();
accountPromise.then(function (account) {
expect(typeof account.prettifyAmount).toEqual('function');
});
$httpBackend.flush();
});
it('should allow for a custom method to be placed at the model level using regexp when several models are requested', function () {
var accountsPromise;
Restangular.addElementTransformer(/^accounts/, false, function (model) {
model.prettifyAmount = function () {};
return model;
});
accountsPromise = Restangular.all('accounts', 1).getList();
accountsPromise.then(function (accounts) {
accounts.forEach(function (account) {
expect(typeof account.prettifyAmount).toEqual('function');
});
});
$httpBackend.flush();
});
});
describe('extendCollection', function () {
it('should be an alias for a specific invocation of addElementTransformer', function () {
var spy = spyOn(Restangular, 'addElementTransformer');
var fn = function (collection) {
collection.totalAmount = function () {};
return collection;
};
Restangular.extendCollection('accounts', fn);
expect(spy).toHaveBeenCalledWith('accounts', true, fn);
});
});
describe('extendModel', function () {
it('should be an alias for a specific invocation of addElementTransformer', function () {
var spy = spyOn(Restangular, 'addElementTransformer');
var fn = function (model) {
model.prettifyAmount = function () {};
return model;
};
Restangular.extendModel('accounts', fn);
expect(spy).toHaveBeenCalledWith('accounts', false, fn);
});
});
describe('headers', function () {
it('should return defaultHeaders', function () {
var defaultHeaders = {
testheader: 'header value'
};
Restangular.setDefaultHeaders(defaultHeaders);
expect(Restangular.defaultHeaders).toEqual(defaultHeaders);
});
it('should pass uppercase methods in X-HTTP-Method-Override', function () {
Restangular.setMethodOverriders(['put']);
$httpBackend.expectPOST('/overriders/1').respond(function (method, url, data, headers) {
expect(headers['X-HTTP-Method-Override']).toBe('PUT');
return {};
});
Restangular.one('overriders', 1).put();
$httpBackend.flush();
});
});
describe('defaultRequestParams', function () {
it('should return defaultRequestParams', function () {
var defaultRequestParams = {
param: 'value'
};
Restangular.setDefaultRequestParams(defaultRequestParams);
expect(Restangular.requestParams.common).toEqual(defaultRequestParams);
});
it('should be able to set default params for get, post, put.. methods separately', function () {
var postParams = {
post: 'value'
},
putParams = {
put: 'value'
};
Restangular.setDefaultRequestParams('post', postParams);
expect(Restangular.requestParams.post).toEqual(postParams);
Restangular.setDefaultRequestParams('put', putParams);
expect(Restangular.requestParams.put).toEqual(putParams);
expect(Restangular.requestParams.common).not.toEqual(putParams);
});
it('should be able to set default params for multiple methods with array', function () {
var defaultParams = {
param: 'value'
};
Restangular.setDefaultRequestParams(['post', 'put'], defaultParams);
expect(Restangular.requestParams.post).toEqual(defaultParams);
expect(Restangular.requestParams.put).toEqual(defaultParams);
expect(Restangular.requestParams.common).not.toEqual(defaultParams);
});
});
describe('withConfig', function () {
it('should create new service with scoped configuration', function () {
var childRestangular = Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setBaseUrl('/api/v1');
});
expect(Restangular.configuration.baseUrl).toEqual('');
expect(childRestangular.configuration.baseUrl).toEqual('/api/v1');
});
it('should allow nested configurations', function () {
var childRestangular = Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setBaseUrl('/api/v1');
});
var grandchildRestangular = childRestangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setRequestSuffix('.json');
});
expect(Restangular.configuration.baseUrl).toEqual('');
expect(Restangular.configuration.suffix).toEqual(null);
expect(childRestangular.configuration.baseUrl).toEqual('/api/v1');
expect(childRestangular.configuration.suffix).toEqual(null);
expect(grandchildRestangular.configuration.baseUrl).toEqual('/api/v1');
expect(grandchildRestangular.configuration.suffix).toEqual('.json');
});
});
describe('Self linking', function () {
it('Should request the link in HAL format', function () {
var linkRestangular = Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setRestangularFields({
selfLink: '_links.self'
});
});
var arr = linkRestangular.all('accountsHAL').getList().$object;
$httpBackend.flush();
var account = arr[0];
$httpBackend.expectPUT('/accountsHAL/martin');
account.name = 'Updated';
account.put();
$httpBackend.flush();
});
});
describe('Singe one (endpoint not expecting an id)', function () {
it('does not use the id for single resource GET', function () {
Restangular.one('info', 0, true).get();
$httpBackend.expectGET('/info');
$httpBackend.flush();
});
it('getRestangularUrl() returns still the url without id after GET', function () {
var record = Restangular.one('info', 0, true);
record.get().then(function (data) {
expect(data.getRestangularUrl()).toEqual('/info');
});
$httpBackend.expectGET('/info');
$httpBackend.flush();
});
it('does not use the id for single nested resource GET', function () {
Restangular.one('accounts', 1).one('info', 0, true).get();
$httpBackend.expectGET('/accounts/1/info');
$httpBackend.flush();
});
it('does not use the id for single resource PUT', function () {
Restangular.one('info', 0, true).put();
$httpBackend.expectPUT('/info');
$httpBackend.flush();
});
});
describe('setSelfLinkAbsoluteUrl', function () {
it('works', function () {
var childRestangular = Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setSelfLinkAbsoluteUrl(false);
});
expect(Restangular.configuration.absoluteUrl).toEqual(true);
expect(childRestangular.configuration.absoluteUrl).toEqual(false);
});
});
describe('Misc', function () {
it('should accept 0 as response', function () {
Restangular.one('misc', 'zero').get().then(function (res) {
expect(res).toEqual(0);
});
$httpBackend.flush();
});
it('Should accept 0 as a proper id in custom requests', function () {
$httpBackend.expectDELETE('/accounts/0').respond(202);
Restangular.all('accounts').customDELETE(0);
$httpBackend.flush();
});
});
describe('testing normalize url', function () {
it('should get a list of objects', function () {
Restangular.all('customers/').getList().then(function (res) {
res.getList({
active: true
});
$httpBackend.expectGET('/customers/?active=true');
//res.getList('publications/', {tags: 'chemistry'});
//$httpBackend.expectGET('/customers/publications/?tags=chemistry');
});
$httpBackend.expectGET('/customers/');
$httpBackend.flush();
});
it('should get a list of objects even if the path has extra slashes', function () {
Restangular.all('customers///').getList().then(function (res) {
res.getList({
active: true
});
$httpBackend.expectGET('/customers/?active=true');
});
$httpBackend.expectGET('/customers/');
$httpBackend.flush();
});
it('should post with slash at the end', function () {
Restangular.all('customers/').getList().then(function (res) {
res.post(testData.newCustomer);
$httpBackend.expectPOST('/customers/');
});
$httpBackend.expectGET('/customers/');
$httpBackend.flush();
});
it('should put with slash at the end', function () {
Restangular.all('customers/').getList().then(function (customers) {
customers[0].put();
$httpBackend.expectPUT('/customers/0');
});
$httpBackend.flush();
});
it('should return a normalized URL even it has extra slashes', function () {
var restangularSpaces = Restangular.one('accounts//', 123).one('buildings//', 456).all('spaces///');
expect(restangularSpaces.getRestangularUrl()).toEqual('/accounts/123/buildings/456/spaces/');
});
it('should create a new service and still working normalized URL', function () {
var newRes = Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setBaseUrl('http://localhost:8080');
});
expect(newRes.configuration.baseUrl).toEqual('http://localhost:8080');
newRes.all('customers////').getList();
$httpBackend.expectGET('http://localhost:8080/customers/');
var newApi = Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setBaseUrl('api.new.domain');
});
expect(newApi.configuration.baseUrl).toEqual('api.new.domain');
newApi.all('customers////').getList();
$httpBackend.expectGET('api.new.domain/customers/');
$httpBackend.flush();
});
it('Should work with absolute URL with //authority', function () {
var newRes = Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setBaseUrl('//localhost:8080');
});
expect(newRes.configuration.baseUrl).toEqual('//localhost:8080');
newRes.all('customers////').getList();
$httpBackend.expectGET('//localhost:8080/customers/').respond([]);
$httpBackend.flush();
});
});
describe('setPlainByDefault', function () {
var plainByDefaultRestangular;
beforeEach(function () {
plainByDefaultRestangular = Restangular.withConfig(function (RestangularConfigurer) {
RestangularConfigurer.setPlainByDefault(true);
});
});
it('should set the property on the configuration', function () {
expect(plainByDefaultRestangular.configuration.plainByDefault).toEqual(true);
});
it('should not add restangularized methods to response object', function () {
plainByDefaultRestangular.one('accounts', 0).get().then(function (account) {
expect(account).toEqual(testData.accountsModel[0]);
});
$httpBackend.flush();
});
it('shoud not add restangularized methods to response collection', function () {
plainByDefaultRestangular.all('accounts').getList().then(function (accounts) {
expect(accounts).toEqual(testData.accountsModel);
});
$httpBackend.flush();
});
describe('with ETag', function () {
beforeEach(function () {
$httpBackend.whenGET('/accounts').respond(
testData.accountsModel,
{'ETag': 'c11ea3f8-3bfd-4be8-a6a6-501dd831b8a4'}
);
$httpBackend.whenGET('/accounts/1').respond(
testData.accountsModel[1],
{'ETag': 'bf79b780-f132-4f44-a9eb-7e6eb4f902b2'}
);
});
it('should not add restangularized ETag to response object', function () {
plainByDefaultRestangular.one('accounts', 0).get().then(function (account) {
expect(account).toEqual(testData.accountsModel[0]);
});
$httpBackend.flush();
});
it('shoud not add restangularized ETag to response collection', function () {
plainByDefaultRestangular.all('accounts').getList().then(function (accounts) {
expect(accounts).toEqual(testData.accountsModel);
});
$httpBackend.flush();
});
});
});
describe('ETags', function () {
beforeEach(function () {
$httpBackend.whenGET('/etagAccounts').respond(
testData.accountsModel,
{'ETag': 'c11ea3f8-3bfd-4be8-a6a6-501dd831b8a4'}
);
$httpBackend.whenGET('/etagAccounts/1').respond(
testData.accountsModel[1],
{'ETag': 'bf79b780-f132-4f44-a9eb-7e6eb4f902b2'}
);
});
it('should include the ETag in the restangularized element', function () {
Restangular.one('etagAccounts', 1).get().then(function (account) {
expect(account.restangularEtag).toEqual('bf79b780-f132-4f44-a9eb-7e6eb4f902b2');
});
$httpBackend.flush();
});
it('should include the ETag in the restangularized collection', function () {
Restangular.all('etagAccounts').getList().then(function (accounts) {
expect(accounts.restangularEtag).toEqual('c11ea3f8-3bfd-4be8-a6a6-501dd831b8a4');
});
$httpBackend.flush();
});
it('should add the If-Match header on PUT requests', function () {
var responseHandler = jasmine.createSpy();
Restangular.one('etagAccounts', 1).get().then(responseHandler);
$httpBackend.flush();
var account = responseHandler.calls.argsFor(0)[0];
$httpBackend.expect(
'PUT',
'/etagAccounts/1',
testData.accountsModel[1],
function (headers) {
return headers['If-Match'] === 'bf79b780-f132-4f44-a9eb-7e6eb4f902b2';
}
).respond(200);
account.save();
$httpBackend.flush();
});
it('should add the If-Match header on DELETE requests', function () {
var responseHandler = jasmine.createSpy();
Restangular.one('etagAccounts', 1).get().then(responseHandler);
$httpBackend.flush();
var account = responseHandler.calls.argsFor(0)[0];
$httpBackend.expect(
'DELETE',
'/etagAccounts/1',
testData.accountsModel[1],
function (headers) {
return headers['If-Match'] === 'bf79b780-f132-4f44-a9eb-7e6eb4f902b2';
}
).respond(200);
account.remove();
$httpBackend.flush();
});
it('should add the If-None-Match header on GET requests for elements', function () {
var responseHandler = jasmine.createSpy();
Restangular.one('etagAccounts', 1).get().then(responseHandler);
$httpBackend.flush();
var account = responseHandler.calls.argsFor(0)[0];
$httpBackend.expect(
'GET',
'/etagAccounts/1',
undefined,
function (headers) {
return headers['If-None-Match'] === 'bf79b780-f132-4f44-a9eb-7e6eb4f902b2';
}
).respond(200);
account.get();
$httpBackend.flush();
});
it('should add the If-None-Match header on GET requests for collections', function () {
var responseHandler = jasmine.createSpy();
Restangular.all('etagAccounts').getList().then(responseHandler);
$httpBackend.flush();
var accounts = responseHandler.calls.argsFor(0)[0];
$httpBackend.expect(
'GET',
'/etagAccounts',
undefined,
function (headers) {
return headers['If-None-Match'] === 'c11ea3f8-3bfd-4be8-a6a6-501dd831b8a4';
}
).respond(200);
accounts.getList();
$httpBackend.flush();
});
});
});