├── env.sh
├── .bowerrc
├── .gitignore
├── docs
├── content
│ └── api
│ │ └── index.ngdoc
└── html
│ └── nav.html
├── .travis.yml
├── .editorconfig
├── CHANGELOG.md
├── bower.json
├── LICENSE.md
├── package.json
├── coffeelint.json
├── karma
└── karma-unit.tpl.js
├── test
├── headers-spec.coffee
├── transform-response-spec.coffee
├── javascript-spec.js
├── remap-spec.coffee
├── relations-spec.coffee
├── basic-spec.coffee
└── basic-fields-spec.coffee
├── README.md
├── dist
├── angular-rest-orm.min.js
└── angular-rest-orm.js
├── Gruntfile.js
└── src
└── ORM.coffee
/env.sh:
--------------------------------------------------------------------------------
1 | export PATH=`pwd`/node_modules/.bin:$PATH
2 |
--------------------------------------------------------------------------------
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "vendor",
3 | "json": "bower.json"
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.sw*
2 | *~
3 | build/
4 | node_modules/
5 | vendor/
6 | /coverage/
7 | /tmp/
8 | /site/
9 |
--------------------------------------------------------------------------------
/docs/content/api/index.ngdoc:
--------------------------------------------------------------------------------
1 | @ngdoc overview
2 | @name API Reference
3 | @description
4 |
5 | #angular REST ORM - API Reference
6 |
7 | Welcome to the Angular REST ORM API reference documentation.
8 |
--------------------------------------------------------------------------------
/docs/html/nav.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.10"
4 |
5 | before_script:
6 | - export DISPLAY=:99.0
7 | - sh -e /etc/init.d/xvfb start
8 | - npm install --quiet -g grunt-cli karma
9 | - npm install
10 |
11 | script: grunt
12 |
13 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 | charset = utf-8
12 | indent_style = space
13 | indent_size = 2
14 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | ### 0.3.1 (2014-09-04)
3 |
4 |
5 |
6 | ## 0.2.0 (2014-08-28)
7 |
8 |
9 |
10 | ## 0.1.0 (2014-08-28)
11 |
12 |
13 | #### Features
14 |
15 | * **all:** provide first draft. ([4120fae4](http://github.com/panta/angular-rest-orm/commit/4120fae467d6288c261a369c7e747a799ef82c42))
16 |
17 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-rest-orm",
3 | "version": "0.4.5",
4 | "main": "dist/angular-rest-orm.min.js",
5 | "authors": [
6 | "Marco Pantaleoni "
7 | ],
8 | "description": "Angular ORM for HTTP REST APIs",
9 | "keywords": [
10 | "angular", "ORM", "REST", "RESTful",
11 | "HTTP", "OOP", "nesting", "model", "resource",
12 | "service"
13 | ],
14 | "license": "MIT",
15 | "homepage": "https://github.com/panta/angular-rest-orm",
16 | "repository": {
17 | "type": "git",
18 | "url": "git://github.com/panta/angular-rest-orm.git"
19 | },
20 | "ignore": [
21 | "**/.*",
22 | "bower.json",
23 | "Gruntfile.js",
24 | "coffeelint.json",
25 | "env.sh",
26 | "karma.conf.js",
27 | "package.json",
28 | "build",
29 | "karma",
30 | "src",
31 | "test",
32 | "vendor"
33 | ],
34 | "dependencies": {},
35 | "devDependencies": {
36 | "angular": "^1.2",
37 | "angular-mocks": "^1.2"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2014 Marco Pantaleoni.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-rest-orm",
3 | "author": {
4 | "name": "Marco Pantaleoni",
5 | "email": "marco.pantaleoni@gmail.com",
6 | "url": "https://github.com/panta"
7 | },
8 | "version": "0.4.5",
9 | "homepage": "https://github.com/panta/angular-rest-orm",
10 | "license": "MIT",
11 | "private": true,
12 | "engines": {
13 | "node": ">= 0.10"
14 | },
15 | "dependencies": {},
16 | "devDependencies": {
17 | "grunt": "~0.4.1",
18 | "grunt-cli": "~0.1.13",
19 | "grunt-contrib-clean": "^0.4.1",
20 | "grunt-contrib-copy": "^0.4.1",
21 | "grunt-contrib-jshint": "^0.4.3",
22 | "grunt-contrib-concat": "^0.3.0",
23 | "grunt-contrib-watch": "^0.4.4",
24 | "grunt-contrib-uglify": "^0.2.7",
25 | "grunt-contrib-coffee": "^0.7.0",
26 | "grunt-coffeelint": "~0.0.10",
27 | "grunt-ng-annotate": "~0.3.2",
28 | "grunt-conventional-changelog": "^0.1.2",
29 | "grunt-bump": "0.0.6",
30 | "grunt-karma": "^0.8.2",
31 | "grunt-git": "~0.2.14",
32 | "karma": "^0.12.9",
33 | "karma-jasmine": "^0.1.5",
34 | "karma-coffee-preprocessor": "^0.2.1",
35 | "karma-firefox-launcher": "^0.1.3",
36 | "karma-phantomjs-launcher": "~0.1.4",
37 | "karma-coverage": "~0.2.6",
38 | "bower": "^1.3.9",
39 | "grunt-ngdocs": "^0.2.3"
40 | },
41 | "scripts": {
42 | "postinstall": "node_modules/.bin/bower install",
43 | "build": "node_modules/.bin/grunt build",
44 | "dist": "node_modules/.bin/grunt dist"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/coffeelint.json:
--------------------------------------------------------------------------------
1 | {
2 | "arrow_spacing": {
3 | "level": "ignore"
4 | },
5 | "camel_case_classes": {
6 | "level": "error"
7 | },
8 | "coffeescript_error": {
9 | "level": "error"
10 | },
11 | "colon_assignment_spacing": {
12 | "level": "ignore",
13 | "spacing": {
14 | "left": 0,
15 | "right": 0
16 | }
17 | },
18 | "cyclomatic_complexity": {
19 | "value": 10,
20 | "level": "ignore"
21 | },
22 | "duplicate_key": {
23 | "level": "error"
24 | },
25 | "empty_constructor_needs_parens": {
26 | "level": "ignore"
27 | },
28 | "indentation": {
29 | "value": 2,
30 | "level": "error"
31 | },
32 | "line_endings": {
33 | "level": "ignore",
34 | "value": "unix"
35 | },
36 | "max_line_length": {
37 | "value": 120,
38 | "level": "error",
39 | "limitComments": true
40 | },
41 | "missing_fat_arrows": {
42 | "level": "ignore"
43 | },
44 | "newlines_after_classes": {
45 | "value": 3,
46 | "level": "ignore"
47 | },
48 | "no_backticks": {
49 | "level": "error"
50 | },
51 | "no_debugger": {
52 | "level": "warn"
53 | },
54 | "no_empty_functions": {
55 | "level": "ignore"
56 | },
57 | "no_empty_param_list": {
58 | "level": "ignore"
59 | },
60 | "no_implicit_braces": {
61 | "level": "ignore",
62 | "strict": true
63 | },
64 | "no_implicit_parens": {
65 | "strict": true,
66 | "level": "ignore"
67 | },
68 | "no_interpolation_in_single_quotes": {
69 | "level": "ignore"
70 | },
71 | "no_plusplus": {
72 | "level": "ignore"
73 | },
74 | "no_stand_alone_at": {
75 | "level": "ignore"
76 | },
77 | "no_tabs": {
78 | "level": "error"
79 | },
80 | "no_throwing_strings": {
81 | "level": "error"
82 | },
83 | "no_trailing_semicolons": {
84 | "level": "error"
85 | },
86 | "no_trailing_whitespace": {
87 | "level": "error",
88 | "allowed_in_comments": false,
89 | "allowed_in_empty_lines": true
90 | },
91 | "no_unnecessary_double_quotes": {
92 | "level": "ignore"
93 | },
94 | "no_unnecessary_fat_arrows": {
95 | "level": "warn"
96 | },
97 | "non_empty_constructor_needs_parens": {
98 | "level": "ignore"
99 | },
100 | "space_operators": {
101 | "level": "ignore"
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/karma/karma-unit.tpl.js:
--------------------------------------------------------------------------------
1 | module.exports = function ( karma ) {
2 | karma.set({
3 | /**
4 | * From where to look for files, starting with the location of this file.
5 | */
6 | basePath: '../',
7 |
8 | /**
9 | * This is the list of file patterns to load into the browser during testing.
10 | */
11 | files: [
12 | <% scripts.forEach( function ( file ) { %>'<%= file %>',
13 | <% }); %>
14 | //'src/**/*.js',
15 | 'src/**/*.coffee',
16 |
17 | // tests
18 | { pattern: 'test/**/*-spec.coffee', included: true },
19 | { pattern: 'test/**/*-spec.js', included: true }
20 | ],
21 | exclude: [
22 | ],
23 |
24 | // frameworks to use
25 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
26 | frameworks: [ 'jasmine' ],
27 |
28 | plugins: [ 'karma-jasmine', 'karma-firefox-launcher', 'karma-phantomjs-launcher', 'karma-coverage', 'karma-coffee-preprocessor' ],
29 | preprocessors: {
30 | // source files, that we wanna generate coverage for
31 | // do not include tests or libraries
32 | // (these files will be instrumented by Istanbul via Ibrik)
33 | 'src/*.coffee': 'coverage',
34 |
35 | // project files will already be converted to
36 | // JavaScript via coverage preprocessor.
37 | // Thus, we'll have to limit the CoffeeScript preprocessor
38 | // to uncovered files (test files).
39 | 'test/**/*.coffee': 'coffee'
40 | },
41 |
42 | // test results reporter to use
43 | // possible values: 'dots', 'progress'
44 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
45 | reporters: ['dots', 'coverage'],
46 |
47 | coffeePreprocessor: {
48 | options: {
49 | sourceMap: true
50 | }
51 | },
52 |
53 | coverageReporter : {
54 | type: 'html',
55 | dir: 'coverage/'
56 | },
57 |
58 | /**
59 | * On which port should the browser connect, on which port is the test runner
60 | * operating, and what is the URL path for the browser to use.
61 | */
62 | port: 9018,
63 | runnerPort: 9100,
64 | urlRoot: '/',
65 |
66 | /**
67 | * Disable file watching by default.
68 | */
69 | autoWatch: false,
70 |
71 | // level of logging
72 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
73 | logLevel: karma.LOG_INFO,
74 |
75 | /**
76 | * The list of browsers to launch to test on. This includes only "Firefox" by
77 | * default, but other browser names include:
78 | * Chrome, ChromeCanary, Firefox, Opera, Safari, PhantomJS
79 | *
80 | * Note that you can also use the executable name of the browser, like "chromium"
81 | * or "firefox", but that these vary based on your operating system.
82 | *
83 | * You may also leave this blank and manually navigate your browser to
84 | * http://localhost:9018/ when you're running tests. The window/tab can be left
85 | * open and the tests will automatically occur there during the build. This has
86 | * the aesthetic advantage of not launching a browser every time you save.
87 | */
88 | browsers: [
89 | //'Firefox',
90 | 'PhantomJS'
91 | ]
92 | });
93 | };
94 |
95 |
--------------------------------------------------------------------------------
/test/headers-spec.coffee:
--------------------------------------------------------------------------------
1 | describe "headers functionality:", ->
2 | $rootScope = undefined
3 | $httpBackend = undefined
4 | $q = undefined
5 | Resource = undefined
6 | Book = undefined
7 |
8 | beforeEach module("restOrm")
9 |
10 | beforeEach inject(($injector) ->
11 | $rootScope = $injector.get("$rootScope")
12 | $httpBackend = $injector.get("$httpBackend")
13 | $q = $injector.get("$q")
14 | Resource = $injector.get("Resource")
15 | )
16 |
17 | describe "function headers", ->
18 | it "should work", ->
19 | class Book extends Resource
20 | @urlEndpoint: '/api/v1/books/'
21 | @defaults:
22 | title: ""
23 | @headers: (info) ->
24 | {
25 | "X-My-Auth": "AAA-...-ZZZ"
26 | "X-Operation": info.what
27 | "X-Method": info.method
28 | "X-Class": info.klass.name
29 | }
30 | $httpBackend.expect('POST', '/api/v1/books/', undefined, (headers) ->
31 | return (headers["X-My-Auth"] == "AAA-...-ZZZ") and
32 | (headers["X-Operation"] == "$save") and
33 | (headers['X-Method'] == 'POST') and
34 | (headers['X-Class'] == 'Book')
35 | ).respond(200, { id: 1, title: "" } )
36 |
37 | book = Book.Create()
38 |
39 | $httpBackend.flush()
40 | return
41 | return
42 |
43 | describe "object headers", ->
44 | it "should work", ->
45 | class Book extends Resource
46 | @urlEndpoint: '/api/v1/books/'
47 | @defaults:
48 | title: ""
49 | @headers:
50 | "X-My-Auth": "AAA-...-ZZZ"
51 | "X-Operation": (info) -> info.what
52 | "X-Method": (info) -> info.method
53 | "X-Class": (info) -> info.klass.name
54 | "X-ID": (info) -> if info.instance?.$id? then "#{info.instance.$id}" else null
55 | $httpBackend.expect('POST', '/api/v1/books/', undefined, (headers) ->
56 | return (headers["X-My-Auth"] == "AAA-...-ZZZ") and
57 | (headers["X-Operation"] == "$save") and
58 | (headers['X-Method'] == 'POST') and
59 | (headers['X-Class'] == 'Book') and
60 | (headers['X-ID'] is undefined)
61 | ).respond(200, { id: 1, title: "" } )
62 |
63 | book = Book.Create()
64 |
65 | $httpBackend.flush()
66 | return
67 | return
68 |
69 | describe "object headers with op/method", ->
70 | it "should work", ->
71 | class Book extends Resource
72 | @urlEndpoint: '/api/v1/books/'
73 | @defaults:
74 | title: ""
75 | @headers:
76 | common:
77 | "X-My-Auth": "AAA-...-ZZZ"
78 | "X-Operation": (info) -> info.what
79 | "X-Method": (info) -> info.method
80 | "X-Class": (info) -> info.klass.name
81 | "X-ID": (info) -> if info.instance?.$id? then "#{info.instance.$id}" else null
82 | "$save":
83 | "X-Save": "TRUE"
84 | POST:
85 | "X-POST": "TRUE"
86 | $httpBackend.expect('POST', '/api/v1/books/', undefined, (headers) ->
87 | return (headers["X-My-Auth"] == "AAA-...-ZZZ") and
88 | (headers["X-Operation"] == "$save") and
89 | (headers['X-ID'] is undefined) and
90 | (headers['X-Save'] == "TRUE") and
91 | (headers['X-POST'] == "TRUE")
92 | ).respond(200, { id: 1, title: "" } )
93 |
94 | book = Book.Create()
95 |
96 | $httpBackend.flush()
97 | return
98 | return
99 |
100 | return
101 |
--------------------------------------------------------------------------------
/test/transform-response-spec.coffee:
--------------------------------------------------------------------------------
1 | describe "response transform:", ->
2 | $rootScope = undefined
3 | $httpBackend = undefined
4 | $q = undefined
5 | Resource = undefined
6 | Book = undefined
7 |
8 | beforeEach module("restOrm")
9 |
10 | beforeEach inject(($injector) ->
11 | $rootScope = $injector.get("$rootScope")
12 | $httpBackend = $injector.get("$httpBackend")
13 | $q = $injector.get("$q")
14 | Resource = $injector.get("Resource")
15 | )
16 |
17 | describe "transformResponse", ->
18 | beforeEach ->
19 | class Book extends Resource
20 | @urlEndpoint: '/api/v1/books/'
21 | @defaults:
22 | title: ""
23 | subtitle: ""
24 | @transformResponse: (res) ->
25 | if res.what == 'All'
26 | newData = []
27 | for k, v of res.data
28 | newData.push v
29 | res.data = newData
30 | else if res.what == 'Get'
31 | res.data = res.data[0]
32 | else if res.what == '$save'
33 | res.data = res.data[0]
34 | return res
35 |
36 | it "should work for .All()", ->
37 | collection = Book.All()
38 |
39 | $httpBackend.when('GET', '/api/v1/books/').respond(200,
40 | {
41 | 0: { id: 1, title: "The Jungle", subtitle: "" },
42 | 1: { id: 2, title: "Robinson Crusoe" }
43 | })
44 | $httpBackend.flush()
45 |
46 | collection.$promise.then jasmine.createSpy('success')
47 | $rootScope.$digest()
48 | expect(collection.length).toEqual(2)
49 | expect(collection[0] instanceof Book).toBeTruthy()
50 | expect(collection[0].$id).toEqual(1)
51 | expect(collection[0].title).toEqual("The Jungle")
52 | expect(collection[1] instanceof Book).toBeTruthy()
53 | expect(collection[1].$id).toEqual(2)
54 | expect(collection[1].title).toEqual("Robinson Crusoe")
55 | return
56 |
57 | it "should work for .Get()", ->
58 | book = Book.Get(1)
59 |
60 | $httpBackend.when('GET', '/api/v1/books/1').respond(200,
61 | {
62 | 0: { id: 1, title: "The Jungle", subtitle: "" }
63 | })
64 | $httpBackend.flush()
65 |
66 | book.$promise.then jasmine.createSpy('success')
67 | $rootScope.$digest()
68 | expect(book.$id).toEqual(1)
69 | expect(book.title).toEqual("The Jungle")
70 | return
71 |
72 | it "should work for .$ave() for fresh instance", ->
73 | book = new Book({ title: "The Jungle" })
74 | book.$save()
75 | $httpBackend.when('POST', '/api/v1/books/').respond(200,
76 | {
77 | 0: { id: 1, title: "The Jungle" }
78 | })
79 | $httpBackend.flush()
80 |
81 | book.$promise.then jasmine.createSpy('success')
82 | $rootScope.$digest()
83 | expect(book.$id).toEqual(1)
84 | expect(book.title).toEqual("The Jungle")
85 | return
86 |
87 | it "should work for .$ave() for existing instance", ->
88 | book = Book.Get(1)
89 | $httpBackend.when('GET', '/api/v1/books/1').respond(200,
90 | {
91 | 0: { id: 1, title: "The Jungle" }
92 | })
93 | $httpBackend.flush()
94 | book.$promise.then jasmine.createSpy('success')
95 | $rootScope.$digest()
96 | book.title = "The Jungle 2.0"
97 | book.$save()
98 | $httpBackend.when('PUT', '/api/v1/books/1').respond(200,
99 | {
100 | 0: { id: 1, title: "The Jungle 2.0" }
101 | })
102 | $httpBackend.flush()
103 | expect(book.$id).toEqual(1)
104 | expect(book.title).toEqual("The Jungle 2.0")
105 | return
106 |
107 | return
108 |
109 | return
110 |
--------------------------------------------------------------------------------
/test/javascript-spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('Use from JavaScript', function() {
4 |
5 | var $rootScope, $httpBackend, $q, Resource;
6 |
7 | beforeEach(module('restOrm'));
8 |
9 | beforeEach(inject(function($injector) {
10 | $rootScope = $injector.get('$rootScope');
11 | $httpBackend = $injector.get('$httpBackend');
12 | $q = $injector.get('$q');
13 | Resource = $injector.get('Resource');
14 | }));
15 |
16 | describe('Resource subclasses', function() {
17 |
18 | it('should be well formed', function() {
19 | var Book = Resource.Subclass();
20 | expect(Book).toBeDefined();
21 | expect(Book.prototype instanceof Resource).toBeTruthy();
22 | });
23 |
24 | it('should be instantiable', function() {
25 | var Book = Resource.Subclass();
26 | expect(Book).toBeDefined();
27 | var book = new Book();
28 | expect(book).toBeDefined();
29 | });
30 |
31 | it('should be able to define instance properties', function() {
32 | var Book = Resource.Subclass({
33 | myVar: 5
34 | });
35 | var book = new Book();
36 | expect(book.myVar).toBeDefined();
37 | expect(book.myVar).toEqual(5);
38 | });
39 |
40 | it('should be able to define class (static) properties', function() {
41 | var Book = Resource.Subclass({}, {
42 | myClassVar: 6
43 | });
44 | var book = new Book();
45 | expect(Book.myClassVar).toBeDefined();
46 | expect(Book.myClassVar).toEqual(6);
47 | expect(book.myClassVar).toBeUndefined();
48 | });
49 |
50 | it('should be able to define and call methods', function() {
51 | var Book = Resource.Subclass({
52 | myMethod: function() {
53 | return 9;
54 | }
55 | });
56 | var book = new Book();
57 | expect(book.myMethod).toBeDefined();
58 | expect(book.myMethod()).toEqual(9);
59 | });
60 |
61 | it('should be able to define and call class (static) methods', function() {
62 | var Book = Resource.Subclass({}, {
63 | MyClassMethod: function() {
64 | return 11;
65 | }
66 | });
67 | expect(Book.MyClassMethod).toBeDefined();
68 | expect(Book.MyClassMethod()).toEqual(11);
69 | var book = new Book();
70 | expect(book.MyClassMethod).toBeUndefined();
71 | });
72 |
73 | it('should be able to call a Resource class method', function() {
74 | var Book = Resource.Subclass();
75 | expect(Book).toBeDefined();
76 | var book = Book.Get(1);
77 | expect(book).toBeDefined();
78 | });
79 |
80 | it('should be able to use $super', function() {
81 | var Book = Resource.Subclass({
82 | $save: function() {
83 | this.abcd = 456;
84 | return this.$super('$save').apply(this, arguments);
85 | }
86 | }, {
87 | urlEndpoint: '/api/v1/books/'
88 | });
89 |
90 | var book = new Book({ title: "The Jungle" });
91 | book.$save();
92 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle" });
93 | $httpBackend.flush();
94 | book.$promiseDirect.then(jasmine.createSpy('success'));
95 | $rootScope.$digest();
96 | expect(book.$id).toBeDefined();
97 | expect(book.$id).toEqual(1);
98 | expect(book.abcd).toBeDefined();
99 | expect(book.abcd).toEqual(456);
100 | });
101 |
102 | it('should call $initialize', function() {
103 | var Book = Resource.Subclass({
104 | $initialize: function() { this.abc = 42; }
105 | });
106 | spyOn(Book.prototype, '$initialize').andCallThrough()
107 | var book = new Book();
108 | expect(Book.prototype.$initialize).toHaveBeenCalled();
109 | expect(book.abc).toBeDefined();
110 | expect(book.abc).toEqual(42);
111 | });
112 |
113 | });
114 |
115 | });
116 |
--------------------------------------------------------------------------------
/test/remap-spec.coffee:
--------------------------------------------------------------------------------
1 | describe "field mapping functionality:", ->
2 | $rootScope = undefined
3 | $httpBackend = undefined
4 | $q = undefined
5 | Resource = undefined
6 | Book = undefined
7 | Author = undefined
8 | Tag = undefined
9 |
10 | beforeEach module("restOrm")
11 |
12 | beforeEach inject(($injector) ->
13 | $rootScope = $injector.get("$rootScope")
14 | $httpBackend = $injector.get("$httpBackend")
15 | $q = $injector.get("$q")
16 | Resource = $injector.get("Resource")
17 |
18 | class Author extends Resource
19 | @urlEndpoint: '/api/v1/authors/'
20 | @fields:
21 | name: { remote: 'NOME', default: "" }
22 |
23 | class Tag extends Resource
24 | @urlEndpoint: '/api/v1/tags/'
25 | @fields:
26 | name: { remote: 'NOME', default: "" }
27 |
28 | class Book extends Resource
29 | @urlEndpoint: '/api/v1/books/'
30 | @fields:
31 | title: { remote: 'TITOLO', default: "" }
32 | author:
33 | remote: 'AUTORE'
34 | type: Resource.Reference
35 | model: Author
36 | default: null
37 | tags:
38 | remote: 'ETICHETTE'
39 | type: Resource.ManyToMany
40 | model: Tag
41 | default: []
42 | )
43 |
44 | describe "Resource models", ->
45 | it "should properly create instances from passed data", ->
46 | book = new Book({ title: "The Jungle" })
47 | expect(book.title).toEqual("The Jungle")
48 | expect(book.author).toEqual(null)
49 | return
50 |
51 | it "should remap field names on read", ->
52 | $httpBackend.expect('GET', '/api/v1/books/1').respond(200, { id: 1, TITOLO: "Moby Dick", AUTORE: 2, ETICHETTE: [2, 5] } )
53 | $httpBackend.expect('GET', '/api/v1/authors/2').respond(200, { id: 2, NOME: "Herman Melville" } )
54 | $httpBackend.expect('GET', '/api/v1/tags/2').respond(200, { id: 2, NOME: "novel" } )
55 | $httpBackend.expect('GET', '/api/v1/tags/5').respond(200, { id: 5, NOME: "fiction" } )
56 |
57 | book = Book.Get(1)
58 |
59 | book.$promise.then ->
60 | expect(book).toBeDefined()
61 | expect(book instanceof Book).toBeTruthy()
62 | expect(book.$id).toEqual(1)
63 | expect(book.title).toEqual("Moby Dick")
64 | expect(book.author).toBeDefined()
65 | expect(book.author instanceof Author).toBeTruthy()
66 | expect(book.author.$id).toEqual(2)
67 | expect(book.author.name).toEqual("Herman Melville")
68 | expect(book.tags.length).toEqual(2)
69 | expect(book.tags[0] instanceof Tag).toBeTruthy()
70 | expect(book.tags[0].$id).toEqual(2)
71 | expect(book.tags[0].name).toEqual("novel")
72 | expect(book.tags[1] instanceof Tag).toBeTruthy()
73 | expect(book.tags[1].$id).toEqual(5)
74 | expect(book.tags[1].name).toEqual("fiction")
75 |
76 | $rootScope.$digest()
77 | $httpBackend.flush()
78 | return
79 |
80 | it "should remap field names on save (empty relations)", ->
81 | book = new Book({ title: "The Jungle" })
82 | expect(book.title).toEqual("The Jungle")
83 |
84 | $httpBackend.expect('POST', '/api/v1/books/', {
85 | TITOLO: "The Jungle"
86 | AUTORE: null
87 | ETICHETTE: []
88 | }).respond(200, { id: 1, TITOLO: "The Jungle", AUTORE: null } )
89 |
90 | book.$save().$promise.then ->
91 | expect(book.$id).toEqual(1)
92 | expect(book.title).toEqual("The Jungle")
93 | expect(book.author).toEqual(null)
94 |
95 | $rootScope.$digest()
96 | $httpBackend.flush()
97 | return
98 |
99 | it "should remap field names on save (non-empty relations)", ->
100 | $httpBackend.expect('GET', '/api/v1/authors/3').respond(200, { id: 3, NOME: "Upton Sinclair" } )
101 | $httpBackend.expect('GET', '/api/v1/tags/2').respond(200, { id: 2, NOME: "novel" } )
102 | $httpBackend.expect('GET', '/api/v1/tags/5').respond(200, { id: 5, NOME: "fiction" } )
103 |
104 | author = Author.Get(3)
105 | tag_2 = Tag.Get(2)
106 | tag_5 = Tag.Get(5)
107 |
108 | $rootScope.$digest()
109 | $httpBackend.flush()
110 |
111 | $q.all([ author.$promise, tag_2.$promise, tag_5.$promise ]).then ->
112 | book = new Book({ title: "The Jungle", author: author, tags: [tag_2, tag_5] })
113 |
114 | $httpBackend.expect('POST', '/api/v1/books/', {
115 | TITOLO: "The Jungle"
116 | AUTORE: 3
117 | ETICHETTE: [2, 5]
118 | }).respond(200, { id: 1, TITOLO: "The Jungle", AUTORE: 3, ETICHETTE: [2, 5] } )
119 |
120 | book.$save().$promise.then ->
121 | expect(book.$id).toEqual(1)
122 | expect(book.title).toEqual("The Jungle")
123 | expect(book.author).toBeDefined()
124 | expect(book.author instanceof Author).toBeTruthy()
125 | expect(book.author.$id).toEqual(3)
126 | expect(book.author.name).toEqual("Upton Sinclair")
127 | expect(angular.isArray(book.tags)).toBeTruthy()
128 | expect(book.tags.length).toEqual(2)
129 | expect(book.tags[0] instanceof Tag).toBeTruthy()
130 | expect(book.tags[0].$id).toEqual(2)
131 | expect(book.tags[0].name).toEqual("novel")
132 | expect(book.tags[1] instanceof Tag).toBeTruthy()
133 | expect(book.tags[1].$id).toEqual(5)
134 | expect(book.tags[1].name).toEqual("fiction")
135 |
136 | $rootScope.$digest()
137 | $httpBackend.flush()
138 | return
139 |
140 | return
141 |
142 | return
143 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Angular REST ORM
2 | ================
3 |
4 | [](https://travis-ci.org/panta/angular-rest-orm) [](http://badge.fury.io/bo/angular-rest-orm)
5 |
6 | Angular REST ORM provides an easy to use, Active Record-like, ORM for your RESTful APIs.
7 |
8 | It's meant to be more natural and fun to use than `$resource`.
9 |
10 | It supports advanced features, such as collections and relations.
11 |
12 | ```javascript
13 | var Book = Resource.Subclass({}, {
14 | urlEndpoint: '/api/v1/books/',
15 | defaults: {
16 | author: "",
17 | title: "",
18 | subtitle: ""
19 | }
20 | });
21 |
22 | var book = new Book({ 'title': "Moby Dick" });
23 | book.$save();
24 | book.$promise.then(function() {
25 | $log.info("Book saved on server with id " + book.$id);
26 | });
27 |
28 | book = Book.Get(2);
29 | book.$promise.then(function() {
30 | $log.info("Got book with id 2");
31 | $log.info("Title: " + book.title);
32 | });
33 |
34 | var books = Book.All();
35 | books.$promise.then(function() {
36 | for (var i = 0; i < books.length; i++) {
37 | var book = books[i];
38 | $log.info("" + book.$id + ": " + book.title);
39 | }
40 | });
41 | ```
42 |
43 | or in CoffeeScript:
44 |
45 | ```coffeescript
46 | class Book extends Resource
47 | @urlEndpoint: '/api/v1/books/'
48 | @defaults:
49 | author: ""
50 | title: ""
51 | subtitle: ""
52 |
53 | book = new Book({ 'title': "Moby Dick" })
54 | book.$save()
55 | book.$promise.then ->
56 | $log.info "Book saved on server with id #{book.$id}"
57 |
58 | book = Book.Get(2)
59 | book.$promise.then ->
60 | $log.info "Got book with id 2"
61 | $log.info "Title: #{book.title}"
62 |
63 | books = Book.All()
64 | books.$promise.then ->
65 | for book in books
66 | $log.info "#{book.$id}: #{book.title}"
67 | ```
68 |
69 | ## Features
70 |
71 | * Usable **easily** both from **JavaScript** and **CoffeeScript**.
72 | * Object oriented ORM, with an **Active Record like feeling**.
73 | * Like with `$resource`, **usable models and collections are returned immediately**.
74 | * In addition, **models and collection also provide promises** to handle completion of transactions.
75 | * **Directly usable in `$routeProvides.resolve`**: this means that the models and collections will be injected into the controller when ready and complete.
76 | * **Support for one-to-many and many-to-many relations.**
77 | * **Automatic fetch of relations.**
78 | * **Array-based collections.**
79 | * **`id` name mapping** (primary key).
80 | * **Re-mapping** of field names between remote endpoint and model.
81 | * Base URL configuration.
82 | * **Custom headers** easily generated via objects and/or functions.
83 | * **Special responses** easily pre-processed via custom handler.
84 | * **Fully tested.**
85 | * **Bower support.**
86 |
87 |
88 | ## Usage
89 |
90 | ### Installation
91 |
92 | Using `bower`
93 |
94 | ```
95 | bower install angular-rest-orm --save
96 | ```
97 |
98 | alternatively you can clone the project from github.
99 |
100 | ### Use
101 |
102 | Include the library in your HTML
103 |
104 | ```html
105 |
106 | ```
107 |
108 | and declare the dependency on `restOrm` in your module
109 |
110 | ```javascript
111 | app = angular.module('MyApp', ['restOrm'])
112 | ```
113 |
114 | ### Relations
115 |
116 | In JavaScript:
117 |
118 | ```javascript
119 | var Author = Resource.Subclass({}, {
120 | urlEndpoint: '/api/v1/authors/',
121 | fields: {
122 | name: { default: "" }
123 | }
124 | });
125 |
126 | var Tag = Resource.Subclass({}, {
127 | urlEndpoint: '/api/v1/tags/',
128 | fields: {
129 | name: { default: "" }
130 | }
131 | });
132 |
133 | var Book = Resource.Subclass({}, {
134 | urlEndpoint: '/api/v1/books/',
135 | fields: {
136 | title: { default: "" },
137 | author: { type: Resource.Reference, model: Author, default: null },
138 | tags: { type: Resource.ManyToMany, model: Tag, default: [] },
139 | }
140 | });
141 |
142 | var books = Book.All();
143 | books.$promise.then(function() {
144 | for (var i = 0; i < books.length; i++) {
145 | var book = books[i];
146 | $log.info(book.title + " author: " + book.author.name);
147 | for (var j = 0; j < book.tags.length; j++) {
148 | var tag = book.tags[j];
149 | $log.info(" tagged " + tag.name);
150 | }
151 | }
152 | });
153 | ```
154 |
155 | or in CoffeeScript:
156 |
157 | ```coffeescript
158 | class Author extends Resource
159 | @urlEndpoint: '/api/v1/authors/'
160 | @fields:
161 | name: { default: "" }
162 |
163 | class Tag extends Resource
164 | @urlEndpoint: '/api/v1/tags/'
165 | @fields:
166 | name: { default: "" }
167 |
168 | class Book extends Resource
169 | @urlEndpoint: '/api/v1/books/'
170 | @fields:
171 | title: { default: "" }
172 | author:
173 | type: Resource.Reference
174 | model: Author
175 | default: null
176 | tags:
177 | type: Resource.ManyToMany
178 | model: Tag
179 | default: []
180 |
181 | books = Book.All()
182 | books.$promise.then ->
183 | for book in books
184 | $log.info "#{book.title} author: #{book.author.name}"
185 | for tag in book.tags
186 | $log.info " tagged #{tag.name}"
187 | ```
188 |
189 | ## Reference documentation
190 |
191 | Please see the [API reference documentaion][API-docs].
192 |
193 | ## Get help
194 |
195 | If you need assistance, please open a ticket on the [GitHub issues page][issues].
196 |
197 | ## How to help
198 |
199 | We need you!
200 |
201 | Documentation and examples would need some love, so if you want to help,
202 | please head to GitHub issues [#1][issue-1] and [#2][issue-1] and ask
203 | how you could contribute.
204 |
205 | ## Alternatives
206 |
207 | If Angular REST ORM is not your cup of tea, there are other alternatives:
208 |
209 | * **$resource**: perhaps the most known REST interface for AngularJS, albeit
210 | * **Restmod**: a very good and complete library, similar in spirit to Angular REST ORM but with a different flavour.
211 | * **Restangular**: another popular choice, but without a succulent model abstraction.
212 |
213 | We've got some inspiration from all of these, trying to summarize the best parts of each into a unique, coherent API.
214 |
215 | ## License
216 |
217 | This software is licensed under the terms of the [MIT license](LICENSE.md).
218 |
219 | [repo]: https://github.com/panta/angular-rest-orm
220 | [issues]: https://github.com/panta/angular-rest-orm/issues
221 | [issue-1]: https://github.com/panta/angular-rest-orm/issues/1
222 | [issue-2]: https://github.com/panta/angular-rest-orm/issues/2
223 | [API-docs]: http://panta.github.io/angular-rest-orm/
224 |
--------------------------------------------------------------------------------
/test/relations-spec.coffee:
--------------------------------------------------------------------------------
1 | describe "relations functionality:", ->
2 | $rootScope = undefined
3 | $httpBackend = undefined
4 | $q = undefined
5 | Resource = undefined
6 | Book = undefined
7 | Author = undefined
8 |
9 | beforeEach module("restOrm")
10 |
11 | beforeEach inject(($injector) ->
12 | $rootScope = $injector.get("$rootScope")
13 | $httpBackend = $injector.get("$httpBackend")
14 | $q = $injector.get("$q")
15 | Resource = $injector.get("Resource")
16 | )
17 |
18 | describe "Resource.Get()", ->
19 | it "should fetch reference relations", ->
20 | class Author extends Resource
21 | @urlEndpoint: '/api/v1/authors/'
22 | @defaults:
23 | name: ""
24 | class Book extends Resource
25 | @urlEndpoint: '/api/v1/books/'
26 | @fields:
27 | title: { default: "" }
28 | author: { type: Resource.Reference, model: Author, default: null }
29 |
30 | $httpBackend.expect('GET', '/api/v1/books/1').respond(200, { id: 1, title: "Moby Dick", author: 2 } )
31 | $httpBackend.expect('GET', '/api/v1/authors/2').respond(200, { id: 2, name: "Herman Melville" } )
32 |
33 | book = Book.Get(1)
34 |
35 | book.$promise.then ->
36 | expect(book).toBeDefined()
37 | expect(book instanceof Book).toBeTruthy()
38 | expect(book.$id).toEqual(1)
39 | expect(book.title).toEqual("Moby Dick")
40 | expect(book.author).toBeDefined()
41 | expect(book.author instanceof Author).toBeTruthy()
42 | expect(book.author.$id).toEqual(2)
43 | expect(book.author.name).toEqual("Herman Melville")
44 |
45 | $rootScope.$digest()
46 | $httpBackend.flush()
47 | return
48 |
49 | it "should fetch many-to-many relations", ->
50 | class Tag extends Resource
51 | @urlEndpoint: '/api/v1/tags/'
52 | @defaults:
53 | name: ""
54 | class Book extends Resource
55 | @urlEndpoint: '/api/v1/books/'
56 | @fields:
57 | title: { default: "" }
58 | tags: { type: Resource.ManyToMany, model: Tag, default: null }
59 |
60 | $httpBackend.expect('GET', '/api/v1/books/1').respond(200, { id: 1, title: "Moby Dick", tags: [2, 5] } )
61 | $httpBackend.expect('GET', '/api/v1/tags/2').respond(200, { id: 2, name: "novel" } )
62 | $httpBackend.expect('GET', '/api/v1/tags/5').respond(200, { id: 5, name: "fiction" } )
63 |
64 | book = Book.Get(1)
65 |
66 | book.$promise.then ->
67 | expect(book).toBeDefined()
68 | expect(book instanceof Book).toBeTruthy()
69 | expect(book.$id).toEqual(1)
70 | expect(book.title).toEqual("Moby Dick")
71 | expect(book.tags).toBeDefined()
72 | expect(angular.isArray(book.tags)).toBeTruthy()
73 | expect(book.tags.length).toEqual(2)
74 | expect(book.tags[0] instanceof Tag).toBeTruthy()
75 | expect(book.tags[0].$id).toEqual(2)
76 | expect(book.tags[0].name).toEqual("novel")
77 | expect(book.tags[1] instanceof Tag).toBeTruthy()
78 | expect(book.tags[1].$id).toEqual(5)
79 | expect(book.tags[1].name).toEqual("fiction")
80 |
81 | $rootScope.$digest()
82 | $httpBackend.flush()
83 | return
84 |
85 | describe "Resource .$save()", ->
86 | it "should convert reference relations", ->
87 | class Author extends Resource
88 | @urlEndpoint: '/api/v1/authors/'
89 | @defaults:
90 | name: ""
91 | class Book extends Resource
92 | @urlEndpoint: '/api/v1/books/'
93 | @fields:
94 | title: { default: "" }
95 | author: { type: Resource.Reference, model: Author, default: null }
96 |
97 | $httpBackend.expect('GET', '/api/v1/authors/2').respond(200, { id: 2, name: "Herman Melville" } )
98 | $httpBackend.expect('POST', '/api/v1/books/', (data) ->
99 | return false if not (data and angular.isString(data))
100 | data = JSON.parse(data)
101 | return data and data.author? and (data.author == 2)
102 | ).respond(200, { id: 1, title: "Moby Dick", author: 2 })
103 | $httpBackend.expect('GET', '/api/v1/authors/2').respond(200, { id: 2, name: "Herman Melville" } )
104 |
105 | author = Author.Get(2)
106 | author.$promise.then ->
107 |
108 | book = new Book({ title: "Moby Dick", author: author })
109 | book.$save()
110 |
111 | book.$promise.then ->
112 | expect(book).toBeDefined()
113 | expect(book instanceof Book).toBeTruthy()
114 | expect(book.$id).toEqual(1)
115 | expect(book.title).toEqual("Moby Dick")
116 | expect(book.author).toBeDefined()
117 | expect(book.author instanceof Author).toBeTruthy()
118 | expect(book.author.$id).toEqual(2)
119 | expect(book.author.name).toEqual("Herman Melville")
120 |
121 | $rootScope.$digest()
122 | $httpBackend.flush()
123 | return
124 |
125 | it "should convert many-to-many relations", ->
126 | class Tag extends Resource
127 | @urlEndpoint: '/api/v1/tags/'
128 | @defaults:
129 | name: ""
130 | class Book extends Resource
131 | @urlEndpoint: '/api/v1/books/'
132 | @fields:
133 | title: { default: "" }
134 | tags: { type: Resource.ManyToMany, model: Tag, default: null }
135 |
136 | $httpBackend.expect('GET', '/api/v1/tags/2').respond(200, { id: 2, name: "novel" } )
137 | $httpBackend.expect('GET', '/api/v1/tags/5').respond(200, { id: 5, name: "fiction" } )
138 | $httpBackend.expect('POST', '/api/v1/books/', (data) ->
139 | return false if not (data and angular.isString(data))
140 | data = JSON.parse(data)
141 | return data and data.tags? and angular.isArray(data.tags) and (data.tags[0] == 2) and (data.tags[1] == 5)
142 | ).respond(200, { id: 1, title: "Moby Dick", tags: [2, 5] })
143 | $httpBackend.expect('GET', '/api/v1/tags/2').respond(200, { id: 2, name: "novel" } )
144 | $httpBackend.expect('GET', '/api/v1/tags/5').respond(200, { id: 5, name: "fiction" } )
145 |
146 | tag_2 = Tag.Get(2)
147 | tag_5 = Tag.Get(5)
148 |
149 | $q.all([ tag_2.$promise, tag_5.$promise ]).then ->
150 | book = new Book({ title: "Moby Dick", tags: [ tag_2, tag_5 ] })
151 | book.$save()
152 |
153 | book.$promise.then ->
154 | expect(book).toBeDefined()
155 | expect(book instanceof Book).toBeTruthy()
156 | expect(book.$id).toEqual(1)
157 | expect(book.title).toEqual("Moby Dick")
158 | expect(book.tags).toBeDefined()
159 | expect(angular.isArray(book.tags)).toBeTruthy()
160 | expect(book.tags.length).toEqual(2)
161 | expect(book.tags[0] instanceof Tag).toBeTruthy()
162 | expect(book.tags[0].$id).toEqual(2)
163 | expect(book.tags[0].name).toEqual("novel")
164 | expect(book.tags[1] instanceof Tag).toBeTruthy()
165 | expect(book.tags[1].$id).toEqual(5)
166 | expect(book.tags[1].name).toEqual("fiction")
167 |
168 | $rootScope.$digest()
169 | $httpBackend.flush()
170 | return
171 |
172 | return
173 |
174 | return
175 |
--------------------------------------------------------------------------------
/dist/angular-rest-orm.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Angular ORM for HTTP REST APIs
3 | * @version angular-rest-orm - v0.4.5 - 2014-11-11
4 | * @link https://github.com/panta/angular-rest-orm
5 | * @author Marco Pantaleoni
6 | *
7 | * Copyright (c) 2014 Marco Pantaleoni
8 | * Licensed under the MIT License, http://opensource.org/licenses/MIT
9 | */
10 | var __hasProp={}.hasOwnProperty,__slice=[].slice,__extends=function(a,b){function c(){this.constructor=a}for(var d in b)__hasProp.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a};angular.module("restOrm",[]).factory("Resource",["$http","$q","$rootScope",function(a,b,c){var d,e,f,g,h,i,j;return f=function(a){var b;if(null==a)return!0;if(a.length>0)return!1;if(0===a.length)return!0;for(b in a)if(__hasProp.call(a,b))return!1;return!0},h=function(a,b){return a.slice(0,b.length)===b},e=function(a,b){return""===b||a.slice(-b.length)===b},g=function(a){return null==a?!1:angular.isUndefined(a)||null===a?!1:angular.isObject(a)||angular.isArray(a)?!1:!0},j=function(){var a,b,c,d,f,g,i,j,k,l,m,n,o;if(c=1<=arguments.length?__slice.call(arguments,0):[],g=function(a){return a.replace(/[\/]+/g,"/").replace(/\/\?/g,"?").replace(/\/\#/g,"#").replace(/\:\//g,"://")},k=[],c&&c.length>0){for(j=0;jl;l++)if(b=c[l],null!=b&&b)for(f=b,a=(""+b).split("/"),d=m=0,o=a.length;o>=0?o>m:m>o;d=o>=0?++m:--m)if(".."===a[d])k.pop();else{if("."===a[d]||""===a[d])continue;k.push(a[d])}return i=g(k.join("/")),c&&c.length>=1&&(b=""+c[0],h(b,"//")?i="//"+i:h(b,"/")&&(i="/"+i),f=""+f,e(f,"/")&&!e(i,"/")&&(i+="/")),i},i=function(){return encodeURI(j.apply(null,arguments))},d=function(){function d(a,b){null==a&&(a=null),null==b&&(b={}),this.$meta={persisted:!1,async:{direct:{deferred:null,resolved:!0},m2o:{deferred:null,resolved:!0},m2m:{deferred:null,resolved:!0}}},angular.extend(this.$meta,b),this.$error=null,this.$id=null,this._fromObject(a||{}),this.$promise=null,this.$promiseDirect=null,this._setupPromises(),this._fetchRelations(),"function"==typeof this.$initialize&&this.$initialize.apply(this,arguments)}return d.urlPrefix="",d.urlEndpoint="",d.idField="id",d.fields={},d.defaults={},d.headers={},d.prepareRequest=null,d.transformResponse=null,d.postResponse=null,d.Reference="reference",d.ManyToMany="many2many",d.include=function(a){var b,c,d;if(!a)throw new Error("include(obj) requires obj");for(b in a)c=a[b],"included"!==b&&"extended"!==b&&(this.prototype[b]=c);return null!=(d=a.included)&&d.apply(this),this},d.extend=function(a){var b,c,d;if(!a)throw new Error("extend(obj) requires obj");for(b in a)c=a[b],"included"!==b&&"extended"!==b&&(this[b]=c);return null!=(d=a.extended)&&d.apply(this),this},d.Subclass=function(a,b){var c;return c=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}return __extends(b,a),b}(this),a&&c.include(a),b&&c.extend(b),c.prototype.$super=function(a){return this.constructor.__super__[a]},c},d.Create=function(a,b){var c;return null==a&&(a=null),null==b&&(b={}),a=a||this.defaults,c=new this(a,{persisted:!1}),c.$save(b),c},d.Get=function(b,c){var d,e,f;return null==c&&(c={}),d=new this,f=i(this._GetURLBase(),b),d._setupPromises(),e=this._PrepareRequest({id:b,opts:c,what:"Get",method:"GET",url:f,headers:this._BuildHeaders("Get","GET",null),params:c.params||{},data:c.data||{},item:d}),a({method:e.method,url:e.url,headers:e.headers,params:e.params,data:e.data}).then(function(a){return function(b){var c;return c=a._TransformResponse(e,b),d._fromRemote(c.data),a._PostResponse(c)}}(this),function(a){return d.$error=a,d.resolvePromise(d.$meta.async.direct.deferred,!1),d.resolvePromise(d.$meta.async.m2o.deferred,!1),d.resolvePromise(d.$meta.async.m2m.deferred,!1)}),d},d.All=function(b){var c,d,e;return null==b&&(b={}),c=this._MakeCollection(),e=i(this._GetURLBase()),d=this._PrepareRequest({opts:b,what:"All",method:"GET",url:e,headers:this._BuildHeaders("All","GET",null),params:b.params||{},data:b.data||{},collection:c}),a({method:d.method,url:d.url,headers:d.headers,params:d.params,data:d.data}).then(function(a){return function(b){var e,f,g,h,i;for(e=a._TransformResponse(d,b),i=e.data,g=0,h=i.length;h>g;g++)f=i[g],c.push(a._MakeInstanceFromRemote(f));return a._PostResponse(e),c.$finalize()}}(this),function(a){return function(b){var e;return e=a._TransformResponse(d,b),a._PostResponse(e),c.$finalize(!1,b)}}(this)),c},d.Search=function(b,c,d){var e,f,g;return null==d&&(d={}),e=this._MakeCollection(),g=i(this._GetURLBase(),"search",b,c),f=this._PrepareRequest({field:b,value:c,opts:d,what:"Search",method:"GET",url:g,headers:this._BuildHeaders("Search","GET",null),params:d.params||{},data:d.data||{},collection:e}),a({method:f.method,url:f.url,headers:f.headers,params:f.params,data:f.data}).then(function(a){return function(b){var c,d,g,h,i;for(c=a._TransformResponse(f,b),i=c.data,g=0,h=i.length;h>g;g++)d=i[g],e.push(a._MakeInstanceFromRemote(d));return a._PostResponse(c),e.$finalize()}}(this),function(a){return function(b){var c;return c=a._TransformResponse(f,b),a._PostResponse(c),e.$finalize(!1,b)}}(this)),e},d.prototype.$save=function(b){var c,d,e,f;return null==b&&(b={}),c=this._toRemoteObject(),this.$meta.persisted&&null!=this.$id?(d="PUT",f=i(this._getURLBase(),this.$id)):(d="POST",this.constructor.idField in c&&delete c[this.constructor.idField],f=i(this._getURLBase())),this._setupPromises(),e=this._prepareRequest({opts:b,what:"$save",method:d,url:f,headers:this._buildHeaders("$save",d),params:b.params||{},data:c,cache:!1,item:this}),a({method:e.method,url:e.url,data:e.data,cache:e.cache,headers:e.headers,params:e.params}).then(function(a){return function(b){var c;return c=a._transformResponse(e,b),a._fromRemote(c.data),a._postResponse(c)}}(this),function(a){return function(b){return a.$error=b,a.resolvePromise(a.$meta.async.direct.deferred,!1),a.resolvePromise(a.$meta.async.m2o.deferred,!1),a.resolvePromise(a.$meta.async.m2m.deferred,!1)}}(this)),this},d._MakeCollection=function(){var a;return a=[],a.$useApplyAsync=!1,a.$meta={model:this,async:{direct:{deferred:b.defer(),resolved:!1},complete:{deferred:b.defer(),resolved:!1}}},a.$error=!1,a.$promise=a.$meta.async.complete.deferred.promise,a.$promiseDirect=a.$meta.async.direct.deferred.promise,a.$getItemsPromises=function(){var b,c,d,e;for(e=[],c=0,d=a.length;d>c;c++)b=a[c],e.push(b.$promise);return e},a.$getItemsPromiseDirects=function(){var b,c,d,e;for(e=[],c=0,d=a.length;d>c;c++)b=a[c],e.push(b.$promiseDirect);return e},a.$_getPromiseForItems=function(){return b.all(a.$getItemsPromises())},a.$_getPromiseDirectForItems=function(){return b.all(a.$getItemsPromiseDirects())},a._resolvePromise=function(b,c){return null==c&&(c=!0),c?b.resolve(a):b.reject(a)},a.resolvePromise=function(b,d){return null==d&&(d=!0),a.$useApplyAsync?c.$applyAsync(function(){return a._resolvePromise(b,d)}):(a._resolvePromise(b,d),c.$$phase||c.$apply()),a},a.$finalize=function(b,c){var d;return null==b&&(b=!0),null==c&&(c=null),d=b,a.$_getPromiseForItems().then(function(){return a.resolvePromise(a.$meta.async.complete.deferred,b),a.$meta.async.complete.resolved=!0},function(){return d=!1,a.resolvePromise(a.$meta.async.complete.deferred,d),a.$meta.async.complete.resolved=!0,a.$error=!0}),a.resolvePromise(a.$meta.async.direct.deferred,b),a.$meta.async.direct.resolved=!0,a.$error=!b&&c?c:!(b&&d),a},a},d._MakeInstanceFromRemote=function(a){var b;return b=new this,b._setupPromises(),b._fromRemote(a),b},d._GetURLBase=function(){return j(this.urlPrefix,this.urlEndpoint)},d._BuildHeaders=function(a,b,c){var d,e;return null==a&&(a=null),null==b&&(b=null),null==c&&(c=null),null==this.headers?{}:angular.isFunction(this.headers)?this.headers.call(this,{klass:this,what:a,method:b,instance:c}):angular.isObject(this.headers)?(e=function(d){return function(e,f){var g,h,i;for(h in f)i=f[h],g=angular.isFunction(i)?i.call(d,{klass:d,what:a,method:b,instance:c}):i,null!==g&&(e[h]=g);return e}}(this),d={},"common"in this.headers||null!=a&&a in this.headers||null!=b&&b in this.headers?("common"in this.headers&&e(d,this.headers.common),null!=a&&a in this.headers&&e(d,this.headers[a]),null!=b&&b in this.headers&&e(d,this.headers[b])):e(d,this.headers),d):{}},d._PrepareRequest=function(a){return a.klass=this,null!=this.prepareRequest&&angular.isFunction(this.prepareRequest)?this.prepareRequest.call(this,a):a},d.prototype._prepareRequest=function(a){return a.instance=this,this.constructor._PrepareRequest(a)},d._TransformResponse=function(a,b,c){var d;return null==c&&(c=null),d=angular.extend({},a,{request:a,response:b,data:b.data}),d.klass=this,d.instance=c,d.data=d.response.data,d.status=d.response.status,d.headers=d.response.headers,d.config=d.response.config,d.statusText=d.response.statusText,null!=this.transformResponse&&angular.isFunction(this.transformResponse)?this.transformResponse.call(this,d):d},d.prototype._transformResponse=function(a,b){return this.constructor._TransformResponse(a,b,this)},d._PostResponse=function(a){return a.klass=this,null!=this.postResponse&&angular.isFunction(this.postResponse)?this.postResponse.call(this,a):a},d.prototype._postResponse=function(a){return a.instance=this,this.constructor._PostResponse(a)},d.prototype._buildHeaders=function(a,b){return null==a&&(a=null),null==b&&(b=null),this.constructor._BuildHeaders(a,b,this)},d.prototype._setupPromises=function(){var a;return a=!1,(this.$meta.async.direct.resolved||null==this.$meta.async.direct.deferred)&&(this.$meta.async.direct.deferred=b.defer(),this.$meta.async.direct.resolved=!1,a=!0,this.$promiseDirect=this.$meta.async.direct.deferred.promise.then(function(a){return function(){return a.$meta.async.direct.resolved=!0,a}}(this))),(this.$meta.async.m2o.resolved||null==this.$meta.async.m2o.deferred)&&(this.$meta.async.m2o.deferred=b.defer(),this.$meta.async.m2o.resolved=!1,a=!0,this.$meta.async.m2o.deferred.promise.then(function(a){return function(){return a.$meta.async.m2o.resolved=!0,a}}(this))),(this.$meta.async.m2m.resolved||null==this.$meta.async.m2m.deferred)&&(this.$meta.async.m2m.deferred=b.defer(),this.$meta.async.m2m.resolved=!1,a=!0,this.$meta.async.m2m.deferred.promise.then(function(a){return function(){return a.$meta.async.m2m.resolved=!0,a}}(this))),a&&(this.$promise=b.all([this.$meta.async.direct.deferred.promise,this.$meta.async.m2o.deferred.promise,this.$meta.async.m2m.deferred.promise]).then(function(a){return function(){return a.$meta.async.direct.resolved=!0,a.$meta.async.m2o.resolved=!0,a.$meta.async.m2m.resolved=!0,a}}(this))),this},d.prototype._getURLBase=function(){return j(this.constructor.urlPrefix,this.constructor.urlEndpoint)},d.prototype._fetchRelations=function(){return null!=this.$id&&(this._fetchReferences(),this._fetchM2M()),this},d.prototype._fetchReferences=function(){var a,c,d,e;c=function(a,b,c){var d,e,f;return d=b.name,d in a&&null!=a[d]&&g(a[d])?(f=a[d],e=b.model.Get(f),a[d]=e,c.push(e.$promise)):void 0},e=[];for(d in this.constructor.fields)a=this._getField(d),a.type===this.constructor.Reference&&c(this,a,e);return b.all(e).then(function(a){return function(){return a.resolvePromise(a.$meta.async.m2o.deferred)}}(this),function(a){return function(){return a.resolvePromise(a.$meta.async.m2o.deferred,!1)}}(this)),this},d.prototype._fetchM2M=function(){var a,c,d,e,f,g,h,i;d=function(a,b,c,d){var e,f,g,h,i,j,k,l;if(e=b.name,e in a&&null!=a[e]&&angular.isArray(a[e])){for(i=[],h=b.model._MakeCollection(),l=a[e],j=0,k=l.length;k>j;j++)g=l[j],f=b.model.Get(g),h.push(f),i.push(f.$promise);return a[e]=h,c.push(h.$promise),d.push(h)}return a[e]=[]},f=[],a=[];for(e in this.constructor.fields)c=this._getField(e),c.type===this.constructor.ManyToMany&&d(this,c,f,a);for(b.all(f).then(function(a){return function(){return a.resolvePromise(a.$meta.async.m2m.deferred)}}(this),function(a){return function(){return a.resolvePromise(a.$meta.async.m2m.deferred,!1)}}(this)),h=0,i=a.length;i>h;h++)g=a[h],g.$finalize();return this},d.prototype._fromRemote=function(a){return this._fromRemoteObject(a),this.$meta.persisted=!0,this.resolvePromise(this.$meta.async.direct.deferred),this._fetchRelations(),this},d.prototype._resolvePromise=function(a,b){return null==b&&(b=!0),b?a.resolve(this):a.reject(this)},d.prototype.resolvePromise=function(a,b){return null==b&&(b=!0),this.$useApplyAsync?c.$applyAsync(function(c){return function(){return c._resolvePromise(a,b)}}(this)):(this._resolvePromise(a,b),c.$$phase||c.$apply()),this},d.prototype._getFields=function(){var a,b,c,d;a={},d=this.constructor.defaults;for(b in d)c=d[b],a[b]={"default":c};return this.constructor.idField in a||(a[this.constructor.idField]={"default":null}),angular.extend(a,this.constructor.fields),a},d.prototype._getField=function(a){var b;return b={name:a,remote:a,type:null,model:null},a in this.constructor.fields?angular.extend(b,this.constructor.fields[a]||{}):b},d.prototype._toObject=function(){var a,b,c,e,f,g,h,i;c={};for(b in this)if(__hasProp.call(this,b)&&(f=this[b],"$id"!==b&&"$meta"!==b&&"constructor"!==b&&"__proto__"!==b&&(a=this._getField(b),c[b]=f,f)))if(a.type===this.constructor.Reference)(angular.isObject(f)||f instanceof d)&&(c[b]=null!=f.$id?f.$id:null);else if(a.type===this.constructor.ManyToMany){for(g=angular.isArray(f)?f:[f],e=[],h=0,i=g.length;i>h;h++)f=g[h],e.push(angular.isObject(f)||f instanceof d?null!=f.$id?f.$id:null:f);c[b]=e}return c},d.prototype._fromObject=function(a){var b,c,d,e;b=angular.extend({},this.constructor.defaults,a||{});for(d in b)e=b[d],"$id"!==d&&"$meta"!==d&&"constructor"!==d&&"__proto__"!==d&&(this[d]=e);for(d in this.constructor.fields)c=this._getField(d),"$id"!==d&&"$meta"!==d&&"constructor"!==d&&"__proto__"!==d&&!(d in b)&&"default"in c&&(this[d]=c["default"]);return this},d.prototype._toRemoteObject=function(){var a,b,c,e,f,g,h,i;c={};for(b in this)if(__hasProp.call(this,b)&&(f=this[b],"$id"!==b&&"$meta"!==b&&"constructor"!==b&&"__proto__"!==b&&(a=this._getField(b),c[a.remote]=f,f)))if(a.type===this.constructor.Reference)(angular.isObject(f)||f instanceof d)&&(c[a.remote]=null!=f.$id?f.$id:null);else if(a.type===this.constructor.ManyToMany){for(g=angular.isArray(f)?f:[f],e=[],h=0,i=g.length;i>h;h++)f=g[h],e.push(angular.isObject(f)||f instanceof d?null!=f.$id?f.$id:null:f);c[a.remote]=e}return c},d.prototype._fromRemoteObject=function(a){var b,c,d,e,g,h;if(f(this.constructor.fields)){b=angular.extend({},this.constructor.defaults,a||{});for(e in b)g=b[e],"$id"!==e&&"$meta"!==e&&"constructor"!==e&&"__proto__"!==e&&(this[e]=g)}else{b=angular.extend({},a||{}),d=this._getFields();for(e in d)c=this._getField(e),"$id"!==e&&"$meta"!==e&&"constructor"!==e&&"__proto__"!==e&&(c.remote in b?this[e]=b[c.remote]:"default"in c&&(this[e]=c["default"]));h=this.constructor.defaults;for(e in h)g=h[e],e in this||(this[e]=g)}return this.constructor.idField in b&&(this.$id=a[this.constructor.idField]),this},d}()}]);
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function (grunt) {
2 |
3 | /**
4 | * This is the configuration object Grunt uses to give each plugin its
5 | * instructions.
6 | */
7 | grunt.initConfig({
8 | /**
9 | * We read in our `package.json` file so we can access the package name and
10 | * version. It's already there, so we don't repeat ourselves here.
11 | */
12 | pkg: grunt.file.readJSON('bower.json'),
13 |
14 | build_dir: 'build',
15 | dist_dir: 'dist',
16 | src_files: {
17 | js: [ 'src/**/*.js' ],
18 | coffee: [ 'src/**/*.coffee' ]
19 | },
20 |
21 | vendor_files: {
22 | js: [
23 | 'vendor/angular/angular.js'
24 | ]
25 | },
26 |
27 | // used only during testing
28 | test_files: {
29 | js: [
30 | 'vendor/angular-mocks/angular-mocks.js'
31 | ]
32 | },
33 |
34 | /**
35 | * The banner is the comment that is placed at the top of our compiled
36 | * source files. It is first processed as a Grunt template, where the `<%=`
37 | * pairs are evaluated based on this very configuration object.
38 | */
39 | meta: {
40 | banner:
41 | '/**\n' +
42 | ' * <%= pkg.description %>\n' +
43 | ' * @version <%= pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' +
44 | ' * @link <%= pkg.homepage %>\n' +
45 | ' * @author <%= pkg.authors.join(", ") %>\n' +
46 | ' *\n' +
47 | ' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.authors.join(", ") %>\n' +
48 | ' * Licensed under the MIT License, http://opensource.org/licenses/MIT\n' +
49 | ' */\n'
50 | },
51 |
52 | /**
53 | * Creates a changelog on a new version.
54 | */
55 | changelog: {
56 | options: {
57 | dest: 'CHANGELOG.md',
58 | template: 'changelog.tpl'
59 | }
60 | },
61 |
62 | /**
63 | * Increments the version number, etc.
64 | */
65 | bump: {
66 | options: {
67 | files: [
68 | "package.json",
69 | "bower.json"
70 | ],
71 | updateConfigs: ['pkg'],
72 | commit: false,
73 | commitMessage: 'chore(release): v%VERSION%',
74 | commitFiles: [
75 | "package.json",
76 | "bower.json"
77 | ],
78 | createTag: false,
79 | tagName: '%VERSION%',
80 | tagMessage: 'Release %VERSION% version',
81 | push: false,
82 | pushTo: 'origin'
83 | }
84 | },
85 |
86 | gitcommit: {
87 | bump: {
88 | options: {
89 | message: 'chore(release): v<%= pkg.version %>'
90 | },
91 | files: {
92 | src: [
93 | "package.json",
94 | "bower.json",
95 | '<%= dist_dir %>/<%= pkg.name %>.js',
96 | '<%= dist_dir %>/<%= pkg.name %>.min.js'
97 | ]
98 | }
99 | }
100 | },
101 |
102 | gittag: {
103 | bump: {
104 | options: {
105 | tag: '<%= pkg.version %>',
106 | message: 'Release <%= pkg.version %> version'
107 | }
108 | }
109 | },
110 |
111 | gitpush: {
112 | bump_branch: {
113 | options: {
114 | remote: 'origin'
115 | }
116 | },
117 | bump_tags: {
118 | options: {
119 | remote: 'origin',
120 | tags: true
121 | }
122 | }
123 | },
124 |
125 | ngdocs: {
126 | options: {
127 | dest: 'tmp',
128 | navTemplate: 'docs/html/nav.html',
129 | html5Mode: false,
130 | title: false
131 | // startPage: '/guide',
132 | // scripts: [
133 | // '//code.angularjs.org/1.2.23/angular.min.js'
134 | // ],
135 | // styles: ['docs/css/styles.css']
136 | },
137 | api: {
138 | src: [
139 | 'build/src/**/*.js',
140 | 'docs/content/api/*.ngdoc'
141 | ],
142 | title: 'API Reference'
143 | }
144 | // guide: {
145 | // src: ['docs/content/guide/*.ngdoc'],
146 | // title: 'Guide'
147 | // }
148 | },
149 |
150 | /**
151 | * The directories to delete when `grunt clean` is executed.
152 | */
153 | clean: [
154 | '<%= build_dir %>',
155 | '<%= dist_dir %>'
156 | ],
157 |
158 | /**
159 | * The `copy` task just copies files from A to B. We use it here to copy
160 | * our project assets (images, fonts, etc.) and javascripts into
161 | * `build_dir`, and then to copy the assets to `dist_dir`.
162 | */
163 | copy: {
164 | build: {
165 | files: [
166 | {
167 | src: [ '<%= src_files.js %>' ],
168 | dest: '<%= build_dir %>/',
169 | cwd: '.',
170 | expand: true
171 | }
172 | ]
173 | }
174 | },
175 |
176 | /**
177 | * `grunt concat` concatenates multiple source files into a single file.
178 | */
179 | concat: {
180 | /**
181 | * The `dist_js` target is the concatenation of our application source
182 | * code and all specified vendor source code into a single file.
183 | */
184 | dist_js: {
185 | options: {
186 | banner: '<%= meta.banner %>'
187 | },
188 | src: [
189 | 'module.prefix',
190 | '<%= build_dir %>/src/**/*.js',
191 | 'module.suffix'
192 | ],
193 | dest: '<%= dist_dir %>/<%= pkg.name %>.js'
194 | },
195 | /**
196 | * The `dist_min_js` target is the concatenation of our application source
197 | * code and all specified vendor source code into a single file and will
198 | * be minified later by uglify.
199 | */
200 | dist_min_js: {
201 | options: {
202 | banner: '<%= meta.banner %>'
203 | },
204 | src: [
205 | 'module.prefix',
206 | '<%= build_dir %>/src/**/*.js',
207 | 'module.suffix'
208 | ],
209 | dest: '<%= dist_dir %>/<%= pkg.name %>.min.js'
210 | }
211 | },
212 |
213 | /**
214 | * `grunt coffee` compiles the CoffeeScript sources. To work well with the
215 | * rest of the build, we have a separate compilation task for sources and
216 | * specs so they can go to different places. For example, we need the
217 | * sources to live with the rest of the copied JavaScript so we can include
218 | * it in the final build, but we don't want to include our specs there.
219 | */
220 | coffee: {
221 | source: {
222 | options: {
223 | bare: true
224 | },
225 | expand: true,
226 | cwd: '.',
227 | src: [ '<%= src_files.coffee %>' ],
228 | dest: '<%= build_dir %>',
229 | ext: '.js'
230 | }
231 | },
232 |
233 | /**
234 | * `ng-min` annotates the sources before minifying. That is, it allows us
235 | * to code without the array syntax.
236 | */
237 | ngAnnotate: {
238 | options: {
239 | // Tells if ngAnnotate should add annotations (true by default).
240 | add: true,
241 | // Tells if ngAnnotate should remove annotations (false by default).
242 | remove: false,
243 | // Switches the quote type for strings in the annotations array to single
244 | // ones; e.g. '$scope' instead of "$scope" (false by default).
245 | singleQuotes: true
246 | },
247 | dist: {
248 | files: [
249 | {
250 | src: [ '<%= src_files.js %>' ],
251 | cwd: '<%= build_dir %>',
252 | dest: '<%= build_dir %>',
253 | expand: true
254 | }
255 | ]
256 | }
257 | },
258 |
259 | /**
260 | * Minify the sources!
261 | */
262 | uglify: {
263 | dist: {
264 | options: {
265 | banner: '<%= meta.banner %>'
266 | },
267 | files: {
268 | '<%= concat.dist_min_js.dest %>': '<%= concat.dist_min_js.dest %>'
269 | }
270 | }
271 | },
272 |
273 | /**
274 | * `jshint` defines the rules of our linter as well as which files we
275 | * should check. This file, all javascript sources, and all our unit tests
276 | * are linted based on the policies listed in `options`. But we can also
277 | * specify exclusionary patterns by prefixing them with an exclamation
278 | * point (!); this is useful when code comes from a third party but is
279 | * nonetheless inside `src/`.
280 | */
281 | jshint: {
282 | src: [
283 | '<%= src_files.js %>'
284 | ],
285 | // test: [
286 | // '<%= app_files.jsunit %>'
287 | // ],
288 | gruntfile: [
289 | 'Gruntfile.js'
290 | ],
291 | options: {
292 | curly: true,
293 | immed: true,
294 | newcap: true,
295 | noarg: true,
296 | sub: true,
297 | boss: true,
298 | eqnull: true
299 | },
300 | globals: {}
301 | },
302 |
303 | /**
304 | * `coffeelint` does the same as `jshint`, but for CoffeeScript.
305 | * CoffeeScript is not the default in ngBoilerplate, so we're just using
306 | * the defaults here.
307 | */
308 | coffeelint: {
309 | src: {
310 | files: {
311 | src: [ '<%= src_files.coffee %>' ]
312 | }
313 | },
314 | // test: {
315 | // files: {
316 | // src: [ '<%= src_files.coffeeunit %>' ]
317 | // }
318 | // },
319 | options: {
320 | configFile: 'coffeelint.json'
321 | }
322 | },
323 |
324 | /**
325 | * The Karma configurations.
326 | */
327 | karma: {
328 | options: {
329 | configFile: '<%= build_dir %>/karma-unit.js'
330 | },
331 | unit: {
332 | port: 9019,
333 | background: true
334 | },
335 | continuous: {
336 | singleRun: true
337 | }
338 | },
339 |
340 | /**
341 | * This task compiles the karma template so that changes to its file array
342 | * don't have to be managed manually.
343 | */
344 | karmaconfig: {
345 | unit: {
346 | dir: '<%= build_dir %>',
347 | src: [
348 | '<%= vendor_files.js %>',
349 | '<%= test_files.js %>'
350 | ]
351 | }
352 | },
353 |
354 | /**
355 | * And for rapid development, we have a watch set up that checks to see if
356 | * any of the files listed below change, and then to execute the listed
357 | * tasks when they do. This just saves us from having to type "grunt" into
358 | * the command-line every time we want to see what we're working on; we can
359 | * instead just leave "grunt watch" running in a background terminal. Set it
360 | * and forget it, as Ron Popeil used to tell us.
361 | *
362 | * But we don't need the same thing to happen for all the files.
363 | */
364 | delta: {
365 | /**
366 | * By default, we want the Live Reload to work for all tasks; this is
367 | * overridden in some tasks (like this file) where browser resources are
368 | * unaffected. It runs by default on port 35729, which your browser
369 | * plugin should auto-detect.
370 | */
371 | options: {
372 | livereload: true
373 | },
374 |
375 | /**
376 | * When the Gruntfile changes, we just want to lint it. In fact, when
377 | * your Gruntfile changes, it will automatically be reloaded!
378 | */
379 | gruntfile: {
380 | files: 'Gruntfile.js',
381 | tasks: [ 'jshint:gruntfile' ],
382 | options: {
383 | livereload: false
384 | }
385 | },
386 |
387 | /**
388 | * When our JavaScript source files change, we want to run lint them and
389 | * run our unit tests.
390 | */
391 | jssrc: {
392 | files: [
393 | '<%= src_files.js %>'
394 | ],
395 | tasks: [ 'jshint:src', 'karma:unit:run', 'copy:build' ]
396 | },
397 |
398 | /**
399 | * When our CoffeeScript source files change, we want to run lint them and
400 | * run our unit tests.
401 | */
402 | coffeesrc: {
403 | files: [
404 | '<%= src_files.coffee %>'
405 | ],
406 | tasks: [ 'coffeelint:src', 'coffee:source', 'karma:unit:run', 'copy:build' ]
407 | }
408 |
409 | /**
410 | * When a JavaScript unit test file changes, we only want to lint it and
411 | * run the unit tests. We don't want to do any live reloading.
412 | */
413 | // jsunit: {
414 | // files: [
415 | // '<%= app_files.jsunit %>'
416 | // ],
417 | // tasks: [ 'jshint:test', 'karma:unit:run' ],
418 | // options: {
419 | // livereload: false
420 | // }
421 | // },
422 |
423 | /**
424 | * When a CoffeeScript unit test file changes, we only want to lint it and
425 | * run the unit tests. We don't want to do any live reloading.
426 | */
427 | // coffeeunit: {
428 | // files: [
429 | // '<%= app_files.coffeeunit %>'
430 | // ],
431 | // tasks: [ 'coffeelint:test', 'karma:unit:run' ],
432 | // options: {
433 | // livereload: false
434 | // }
435 | // }
436 | }
437 | });
438 |
439 | // Load required Grunt tasks.
440 | grunt.loadNpmTasks('grunt-contrib-clean');
441 | grunt.loadNpmTasks('grunt-contrib-copy');
442 | grunt.loadNpmTasks('grunt-contrib-jshint');
443 | grunt.loadNpmTasks('grunt-contrib-concat');
444 | grunt.loadNpmTasks('grunt-contrib-watch');
445 | grunt.loadNpmTasks('grunt-contrib-uglify');
446 | grunt.loadNpmTasks('grunt-contrib-coffee');
447 | grunt.loadNpmTasks('grunt-conventional-changelog');
448 | grunt.loadNpmTasks('grunt-bump');
449 | grunt.loadNpmTasks('grunt-coffeelint');
450 | grunt.loadNpmTasks('grunt-karma');
451 | grunt.loadNpmTasks('grunt-ng-annotate');
452 | grunt.loadNpmTasks('grunt-git');
453 | grunt.loadNpmTasks('grunt-ngdocs');
454 |
455 | /**
456 | * In order to make it safe to just compile or copy *only* what was changed,
457 | * we need to ensure we are starting from a clean, fresh build. So we rename
458 | * the `watch` task to `delta` (that's why the configuration var above is
459 | * `delta`) and then add a new task called `watch` that does a clean build
460 | * before watching for changes.
461 | */
462 | grunt.renameTask( 'watch', 'delta' );
463 | grunt.registerTask( 'watch', [ 'build', 'karma:unit', 'delta' ] );
464 |
465 | // Default task: `build` and `dist`
466 | grunt.registerTask( 'default', [ 'build', 'dist' ] );
467 |
468 | // build task: get the library ready for development and testing
469 | grunt.registerTask( 'build', [
470 | 'clean', 'jshint', 'coffeelint', 'coffee',
471 | 'copy:build', 'karmaconfig',
472 | 'karma:continuous'
473 | ]);
474 |
475 | // dist task: get the library ready for distribution
476 | grunt.registerTask( 'dist', [
477 | 'ngAnnotate', 'concat:dist_js', 'concat:dist_min_js', 'uglify'
478 | ]);
479 |
480 | // release task: build, dist, bump, commit & tag
481 | grunt.registerTask( 'release', "Perform a release.", function(versionType, incOrCommitOnly) {
482 | var doBump = true, doCommit = true;
483 | if (incOrCommitOnly === 'bump-only') {
484 | grunt.verbose.writeln('Only incrementing the version.');
485 | doCommit = false;
486 | } else if (incOrCommitOnly === 'commit-only') {
487 | grunt.verbose.writeln('Only committing/tagging/pushing.');
488 | doBump = false;
489 | }
490 | if (doBump) {
491 | grunt.verbose.writeln("Bump kind: '" + (versionType || 'patch') + "'");
492 | grunt.task.run('bump:' + (versionType || 'patch'));
493 | }
494 | grunt.task.run([
495 | 'build', 'dist'
496 | ]);
497 | if (doCommit) {
498 | grunt.task.run([
499 | 'gitcommit:bump', 'gittag:bump', 'gitpush:bump_branch', 'gitpush:bump_tags'
500 | ]);
501 | }
502 | });
503 |
504 | // ALIASES
505 | grunt.registerTask('release-bump-only',
506 | "Perform a release incrementing the version only, no tag/commit.",
507 | function(versionType) {
508 | grunt.task.run('release:' + (versionType || '') + ':bump-only');
509 | });
510 |
511 | grunt.registerTask('release-commit',
512 | "Commit, tag, push without incrementing the version.",
513 | 'release::commit-only');
514 |
515 | /**
516 | * A utility function to get all JavaScript sources.
517 | */
518 | function filterForJS ( files ) {
519 | return files.filter( function ( file ) {
520 | return file.match( /\.js$/ );
521 | });
522 | }
523 |
524 | /**
525 | * In order to avoid having to specify manually the files needed for karma to
526 | * run, we use grunt to manage the list for us. The `karma/*` files are
527 | * compiled as grunt templates for use by Karma. Yay!
528 | */
529 | grunt.registerMultiTask( 'karmaconfig', 'Process karma config templates', function () {
530 | var jsFiles = filterForJS( this.filesSrc );
531 |
532 | grunt.file.copy( 'karma/karma-unit.tpl.js', grunt.config( 'build_dir' ) + '/karma-unit.js', {
533 | process: function ( contents, path ) {
534 | return grunt.template.process( contents, {
535 | data: {
536 | scripts: jsFiles
537 | }
538 | });
539 | }
540 | });
541 | });
542 | };
543 |
--------------------------------------------------------------------------------
/test/basic-spec.coffee:
--------------------------------------------------------------------------------
1 | describe "ORM basic functionality:", ->
2 | $rootScope = undefined
3 | $httpBackend = undefined
4 | $q = undefined
5 | Resource = undefined
6 | Book = undefined
7 |
8 | beforeEach module("restOrm")
9 |
10 | beforeEach inject(($injector) ->
11 | $rootScope = $injector.get("$rootScope")
12 | $httpBackend = $injector.get("$httpBackend")
13 | $q = $injector.get("$q")
14 | Resource = $injector.get("Resource")
15 |
16 | class Book extends Resource
17 | @urlEndpoint: '/api/v1/books/'
18 | @defaults:
19 | title: ""
20 | subtitle: ""
21 | author: null
22 | tags: []
23 |
24 | $initialize: ->
25 | @abc = 42
26 | #return
27 | )
28 |
29 | describe "Resource constructor", ->
30 | it "creates new instance", ->
31 | book = new Book()
32 | expect(book).toBeDefined()
33 | expect(book instanceof Book).toBeTruthy()
34 | return
35 | it "calls instance $initialize", ->
36 | spyOn(Book.prototype, '$initialize').andCallThrough()
37 | book = new Book()
38 | expect(Book.prototype.$initialize).toHaveBeenCalled()
39 | expect(book.abc).toBeDefined()
40 | expect(book.abc).toEqual(42)
41 | return
42 | return
43 | it "should return a properfly formed instance", ->
44 | book = new Book()
45 | expect(book.$meta).toBeDefined()
46 | expect(book.$meta.persisted).toBeFalsy()
47 | expect(book.$promise.then).toBeDefined()
48 | expect(book.$id).toBeDefined()
49 | expect(book.$id).toEqual(null)
50 | return
51 | it "should have defaults", ->
52 | book = new Book()
53 | expect(book.title).toEqual("")
54 | expect(book.subtitle).toEqual("")
55 | expect(book.author).toEqual(null)
56 | expect(book.tags).toEqual([])
57 | return
58 |
59 | return
60 |
61 | describe "Resource.Create()", ->
62 | it "returns new instance", ->
63 | book = Book.Create()
64 | expect(book).toBeDefined()
65 | expect(book instanceof Book).toBeTruthy()
66 | return
67 | it "returns new instance with defaults", ->
68 | book = Book.Create()
69 | expect(book.title).toEqual("")
70 | expect(book.subtitle).toEqual("")
71 | expect(book.author).toEqual(null)
72 | expect(book.tags).toEqual([])
73 | return
74 | it "returns new instance with passed values", ->
75 | book = Book.Create({ 'title': "Moby Dick" })
76 | expect(book.title).toEqual("Moby Dick")
77 | return
78 | it "returns new instance with defaults when incomplete values are given", ->
79 | book = Book.Create({ 'title': "Moby Dick" })
80 | expect(book.title).toEqual("Moby Dick")
81 | expect(book.subtitle).toEqual("")
82 | expect(book.author).toEqual(null)
83 | expect(book.tags).toEqual([])
84 | return
85 | it "uses correct REST endpoint", ->
86 | book = Book.Create({ 'title': "Moby Dick" })
87 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle", subtitle: "", author: null, tags: [] } )
88 | $httpBackend.flush()
89 | return
90 | it "fulfills instance $promiseDirect", ->
91 | book = Book.Create({ 'title': "Moby Dick" })
92 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle", subtitle: "", author: null, tags: [] } )
93 | $httpBackend.flush()
94 | handler = jasmine.createSpy('success')
95 | book.$promiseDirect.then handler
96 | $rootScope.$digest()
97 | expect(handler).toHaveBeenCalled()
98 | return
99 | it "fulfills instance $promise", ->
100 | book = Book.Create({ 'title': "Moby Dick" })
101 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle", subtitle: "", author: null, tags: [] } )
102 | $httpBackend.flush()
103 | handler = jasmine.createSpy('success')
104 | book.$promise.then handler
105 | $rootScope.$digest()
106 | expect(handler).toHaveBeenCalled()
107 | return
108 | it "creates an instance with an id", ->
109 | book = Book.Create({ 'title': "Moby Dick" })
110 | expect(book.$id).toEqual(null)
111 | expect(book.$meta.persisted).toBeFalsy()
112 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle", subtitle: "", author: null, tags: [] } )
113 | $httpBackend.flush()
114 | book.$promise.then jasmine.createSpy('success')
115 | $rootScope.$digest()
116 | expect(book.$meta.persisted).toBeTruthy()
117 | expect(book.$id).toEqual(1)
118 | return
119 | it "instance $promise result is the instance itself", ->
120 | book = Book.Create({ 'title': "Moby Dick" })
121 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle", subtitle: "", author: null, tags: [] } )
122 | $httpBackend.flush()
123 | handler = jasmine.createSpy('success')
124 | book.$promise.then handler
125 | $rootScope.$digest()
126 | result = handler.mostRecentCall.args[0]
127 | expect(result).toBe(book)
128 | return
129 | it "should handle params", ->
130 | $httpBackend.expect('POST', '/api/v1/books/?mode=full').respond(200, { id: 1, title: "Moby Dick" })
131 | book = Book.Create { 'title': "Moby Dick" },
132 | params: {mode: "full"}
133 | $httpBackend.flush()
134 | book.$promise.then jasmine.createSpy('success')
135 | $rootScope.$digest()
136 | expect(book.title).toEqual("Moby Dick")
137 | return
138 |
139 | return
140 |
141 | describe "Resource.Get()", ->
142 | it "should return a proper model instance", ->
143 | book = Book.Get(1)
144 | expect(book).toBeDefined()
145 | expect(book instanceof Book).toBeTruthy()
146 | expect(book.$meta).toBeDefined()
147 | expect(book.$promise.then).toBeDefined()
148 | expect(book.$promiseDirect.then).toBeDefined()
149 | return
150 | it "uses correct REST endpoint", ->
151 | $httpBackend.expect('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle", subtitle: "" })
152 | book = Book.Get(1)
153 | $httpBackend.flush()
154 | return
155 | it "fulfills instance $promiseDirect", ->
156 | $httpBackend.expect('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle", subtitle: "", author: null, tags: [] } )
157 | book = Book.Get(1)
158 | $httpBackend.flush()
159 | handler = jasmine.createSpy('success')
160 | book.$promiseDirect.then handler
161 | $rootScope.$digest()
162 | expect(handler).toHaveBeenCalled()
163 | return
164 | it "fulfills instance $promise", ->
165 | book = Book.Get(1)
166 | $httpBackend.when('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle", subtitle: "", author: null, tags: [] } )
167 | $httpBackend.flush()
168 | handler = jasmine.createSpy('success')
169 | book.$promise.then handler
170 | $rootScope.$digest()
171 | expect(handler).toHaveBeenCalled()
172 | return
173 | it "returns an instance with an id", ->
174 | book = Book.Get(1)
175 | $httpBackend.when('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle" })
176 | $httpBackend.flush()
177 | book.$promise.then jasmine.createSpy('success')
178 | $rootScope.$digest()
179 | expect(book.$id).toEqual(1)
180 | return
181 | it "returns instance with values from server", ->
182 | $httpBackend.expect('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle" })
183 | book = Book.Get(1)
184 | $httpBackend.flush()
185 | book.$promise.then jasmine.createSpy('success')
186 | $rootScope.$digest()
187 | expect(book.id).toBeDefined()
188 | expect(book.id).toEqual(1)
189 | expect(book.title).toEqual("The Jungle")
190 | return
191 | it "returns instance with defaults where not provided by server", ->
192 | book = Book.Get(1)
193 | $httpBackend.when('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle" })
194 | $httpBackend.flush()
195 | book.$promise.then jasmine.createSpy('success')
196 | $rootScope.$digest()
197 | expect(book.subtitle).toBeDefined()
198 | expect(book.subtitle).toEqual("")
199 | expect(book.author).toEqual(null)
200 | expect(book.tags).toEqual([])
201 | return
202 | it "should handle params", ->
203 | $httpBackend.expect('GET', '/api/v1/books/1?mode=full', (data) ->
204 | return (data == "{}")
205 | ).respond(200, { id: 1, title: "The Jungle" })
206 | book = Book.Get 1,
207 | params: {mode: "full"}
208 | $httpBackend.flush()
209 | book.$promise.then jasmine.createSpy('success')
210 | $rootScope.$digest()
211 | expect(book.title).toEqual("The Jungle")
212 | return
213 | it "should handle data", ->
214 | $httpBackend.expect('GET', '/api/v1/books/1', (data) ->
215 | return false if not (data and angular.isString(data))
216 | data = JSON.parse(data)
217 | return data and data.mode? and (data.mode == "full")
218 | ).respond(200, { id: 1, title: "The Jungle" })
219 | book = Book.Get 1,
220 | data: {mode: "full"}
221 | $httpBackend.flush()
222 | book.$promise.then jasmine.createSpy('success')
223 | $rootScope.$digest()
224 | expect(book.title).toEqual("The Jungle")
225 | return
226 |
227 | return
228 |
229 | describe "Resource.All()", ->
230 | it "should return a proper collection", ->
231 | collection = Book.All()
232 | expect(collection).toBeDefined()
233 | expect(angular.isArray(collection)).toBeTruthy()
234 | expect(collection.$meta).toBeDefined()
235 | expect(collection.$promise.then).toBeDefined()
236 | expect(collection.$promiseDirect.then).toBeDefined()
237 | return
238 | it "uses correct REST endpoint", ->
239 | collection = Book.All()
240 | $httpBackend.when('GET', '/api/v1/books/').respond(200, [])
241 | $httpBackend.flush()
242 | return
243 | it "fulfills collection $promiseDirect", ->
244 | collection = Book.All()
245 | $httpBackend.when('GET', '/api/v1/books/').respond(200, [ { id: 1, title: "The Jungle", subtitle: "" }, { id: 2, title: "Robinson Crusoe" } ])
246 | $httpBackend.flush()
247 | handler = jasmine.createSpy('success')
248 | collection.$promiseDirect.then handler
249 | $rootScope.$digest()
250 | expect(handler).toHaveBeenCalled()
251 | return
252 | it "fulfills collection $promise", ->
253 | collection = Book.All()
254 | $httpBackend.when('GET', '/api/v1/books/').respond(200, [ { id: 1, title: "The Jungle", subtitle: "" }, { id: 2, title: "Robinson Crusoe" } ])
255 | $httpBackend.flush()
256 | handler = jasmine.createSpy('success')
257 | collection.$promise.then handler
258 | $rootScope.$digest()
259 | expect(handler).toHaveBeenCalled()
260 | return
261 | it "collection $promise result is the collection itself", ->
262 | collection = Book.All()
263 | $httpBackend.when('GET', '/api/v1/books/').respond(200, [ { id: 1, title: "The Jungle", subtitle: "" }, { id: 2, title: "Robinson Crusoe" } ])
264 | $httpBackend.flush()
265 | handler = jasmine.createSpy('success')
266 | collection.$promise.then handler
267 | $rootScope.$digest()
268 | result = handler.mostRecentCall.args[0]
269 | expect(result).toBe(collection)
270 | return
271 | it "fulfills every instance $promiseDirect", ->
272 | collection = Book.All()
273 | $httpBackend.when('GET', '/api/v1/books/').respond(200, [ { id: 1, title: "The Jungle", subtitle: "" }, { id: 2, title: "Robinson Crusoe" } ])
274 | $httpBackend.flush()
275 | collection.$promise.then jasmine.createSpy('success')
276 | $rootScope.$digest()
277 | for instance in collection
278 | handler = jasmine.createSpy('success')
279 | instance.$promiseDirect.then handler
280 | $rootScope.$digest()
281 | expect(handler).toHaveBeenCalled()
282 | return
283 | it "fulfills every instance $promise", ->
284 | collection = Book.All()
285 | $httpBackend.when('GET', '/api/v1/books/').respond(200, [ { id: 1, title: "The Jungle", subtitle: "" }, { id: 2, title: "Robinson Crusoe" } ])
286 | $httpBackend.flush()
287 | collection.$promise.then jasmine.createSpy('success')
288 | $rootScope.$digest()
289 | for instance in collection
290 | handler = jasmine.createSpy('success')
291 | instance.$promise.then handler
292 | $rootScope.$digest()
293 | expect(handler).toHaveBeenCalled()
294 | return
295 | it "should return a collection of instances", ->
296 | collection = Book.All()
297 | $httpBackend.when('GET', '/api/v1/books/').respond(200, [ { id: 1, title: "The Jungle", subtitle: "" }, { id: 2, title: "Robinson Crusoe" } ])
298 | $httpBackend.flush()
299 | collection.$promise.then jasmine.createSpy('success')
300 | $rootScope.$digest()
301 | expect(collection.length).toEqual(2)
302 | expect(collection[0] instanceof Book).toBeTruthy()
303 | expect(collection[0].id).toEqual(1)
304 | expect(collection[0].$id).toBeDefined()
305 | expect(collection[0].$id).toEqual(1)
306 | expect(collection[0].title).toEqual("The Jungle")
307 | expect(collection[1] instanceof Book).toBeTruthy()
308 | expect(collection[1].id).toEqual(2)
309 | expect(collection[1].$id).toBeDefined()
310 | expect(collection[1].$id).toEqual(2)
311 | expect(collection[1].title).toEqual("Robinson Crusoe")
312 | return
313 | it "should handle params", ->
314 | $httpBackend.expect('GET', '/api/v1/books/?title=Robinson+Crusoe', (data) ->
315 | return (data == "{}")
316 | ).respond(200, [ { id: 2, title: "Robinson Crusoe" } ])
317 | collection = Book.All
318 | params: {title: "Robinson Crusoe"}
319 | $httpBackend.flush()
320 | collection.$promise.then jasmine.createSpy('success')
321 | $rootScope.$digest()
322 | expect(collection.length).toEqual(1)
323 | return
324 | it "should handle data", ->
325 | $httpBackend.expect('GET', '/api/v1/books/', (data) ->
326 | return false if not (data and angular.isString(data))
327 | data = JSON.parse(data)
328 | return data and data.title? and (data.title == "Robinson Crusoe")
329 | ).respond(200, [ { id: 2, title: "Robinson Crusoe" } ])
330 | collection = Book.All
331 | data: {title: "Robinson Crusoe"}
332 | $httpBackend.flush()
333 | collection.$promise.then jasmine.createSpy('success')
334 | $rootScope.$digest()
335 | expect(collection.length).toEqual(1)
336 | return
337 |
338 | return
339 |
340 | describe "instance .$save()", ->
341 | it "should return the instance itself", ->
342 | book = new Book()
343 | result = book.$save()
344 | expect(result).toBeDefined()
345 | expect(result instanceof Book).toBeTruthy()
346 | expect(result).toBe(book)
347 | return
348 | it "uses correct REST endpoint for fresh instance", ->
349 | $httpBackend.expect('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle" })
350 | book = new Book({ title: "The Jungle" })
351 | book.$save()
352 | $httpBackend.flush()
353 | return
354 | it "uses correct REST endpoint for existing instance", ->
355 | $httpBackend.expect('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle" })
356 | book = Book.Get(1)
357 | $httpBackend.flush()
358 | book.$promise.then jasmine.createSpy('success')
359 | $rootScope.$digest()
360 | $httpBackend.expect('PUT', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle 2.0" })
361 | book.title = "The Jungle 2.0"
362 | book.$save()
363 | $httpBackend.flush()
364 | return
365 | it "fulfills $promiseDirect for fresh instance", ->
366 | book = new Book({ title: "The Jungle" })
367 | book.$save()
368 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle" })
369 | $httpBackend.flush()
370 | handler = jasmine.createSpy('success')
371 | book.$promiseDirect.then handler
372 | $rootScope.$digest()
373 | expect(handler).toHaveBeenCalled()
374 | return
375 | it "fulfills $promise for fresh instance", ->
376 | book = new Book({ title: "The Jungle" })
377 | book.$save()
378 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle" })
379 | $httpBackend.flush()
380 | handler = jasmine.createSpy('success')
381 | book.$promise.then handler
382 | $rootScope.$digest()
383 | expect(handler).toHaveBeenCalled()
384 | return
385 | it "fulfills $promiseDirect for existing instance", ->
386 | book = Book.Get(1)
387 | $httpBackend.when('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle" })
388 | $httpBackend.flush()
389 | book.$promiseDirect.then jasmine.createSpy('success')
390 | $rootScope.$digest()
391 | book.title = "The Jungle 2.0"
392 | book.$save()
393 | $httpBackend.when('PUT', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle 2.0" })
394 | $httpBackend.flush()
395 | handler = jasmine.createSpy('success')
396 | book.$promiseDirect.then handler
397 | $rootScope.$digest()
398 | expect(handler).toHaveBeenCalled()
399 | return
400 | it "fulfills $promise for existing instance", ->
401 | book = Book.Get(1)
402 | $httpBackend.when('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle" })
403 | $httpBackend.flush()
404 | book.$promise.then jasmine.createSpy('success')
405 | $rootScope.$digest()
406 | book.title = "The Jungle 2.0"
407 | book.$save()
408 | $httpBackend.when('PUT', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle 2.0" })
409 | $httpBackend.flush()
410 | handler = jasmine.createSpy('success')
411 | book.$promise.then handler
412 | $rootScope.$digest()
413 | expect(handler).toHaveBeenCalled()
414 | return
415 | it "fulfills new $promiseDirect", ->
416 | book = new Book({ title: "The Jungle" })
417 | book.$save()
418 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle" })
419 | $httpBackend.flush()
420 | handler = jasmine.createSpy('success')
421 | book.$promiseDirect.then handler
422 | $rootScope.$digest()
423 | expect(handler).toHaveBeenCalled()
424 | book.title = "The Jungle 2.0"
425 | book.$save()
426 | $httpBackend.when('PUT', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle 2.0" })
427 | $httpBackend.flush()
428 | handler = jasmine.createSpy('success')
429 | book.$promiseDirect.then handler
430 | $rootScope.$digest()
431 | expect(handler).toHaveBeenCalled()
432 | return
433 | it "fulfills new $promise", ->
434 | book = new Book({ title: "The Jungle" })
435 | book.$save()
436 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle" })
437 | $httpBackend.flush()
438 | handler = jasmine.createSpy('success')
439 | book.$promise.then handler
440 | $rootScope.$digest()
441 | expect(handler).toHaveBeenCalled()
442 | book.title = "The Jungle 2.0"
443 | book.$save()
444 | $httpBackend.when('PUT', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle 2.0" })
445 | $httpBackend.flush()
446 | handler = jasmine.createSpy('success')
447 | book.$promise.then handler
448 | $rootScope.$digest()
449 | expect(handler).toHaveBeenCalled()
450 | return
451 | it "provides an id to a fresh instance", ->
452 | book = new Book({ title: "The Jungle" })
453 | book.$save()
454 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle" })
455 | $httpBackend.flush()
456 | book.$promise.then jasmine.createSpy('success')
457 | $rootScope.$digest()
458 | expect(book.$id).toBeDefined()
459 | expect(book.$id).toEqual(1)
460 | return
461 | it "saves fresh instance", ->
462 | $httpBackend.expect('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle" })
463 | book = new Book({ title: "The Jungle" })
464 | book.$save()
465 | $httpBackend.flush()
466 | book.$promise.then jasmine.createSpy('success')
467 | $rootScope.$digest()
468 | expect(book.id).toBeDefined()
469 | expect(book.id).toEqual(1)
470 | expect(book.title).toEqual("The Jungle")
471 | return
472 | it "saves changed values of existing instance", ->
473 | book = Book.Get(1)
474 | $httpBackend.when('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle" })
475 | $httpBackend.flush()
476 | book.$promise.then jasmine.createSpy('success')
477 | $rootScope.$digest()
478 | book.title = "The Jungle 2.0"
479 | book.$save()
480 | $httpBackend.when('PUT', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle 2.0" })
481 | $httpBackend.flush()
482 | book.$promise.then jasmine.createSpy('success')
483 | $rootScope.$digest()
484 | expect(book.title).toEqual("The Jungle 2.0")
485 | return
486 | it "should handle params for fresh instance", ->
487 | $httpBackend.expect('POST', '/api/v1/books/?mode=full').respond(200, { id: 1, title: "The Jungle" })
488 | book = new Book({ title: "The Jungle" })
489 | book.$save
490 | params: {mode: "full"}
491 | $httpBackend.flush()
492 | book.$promise.then jasmine.createSpy('success')
493 | $rootScope.$digest()
494 | expect(book.title).toEqual("The Jungle")
495 | return
496 | it "should handle params for existing instance", ->
497 | book = Book.Get(1)
498 | $httpBackend.when('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle" })
499 | $httpBackend.flush()
500 | book.$promise.then jasmine.createSpy('success')
501 | $rootScope.$digest()
502 | $httpBackend.expect('PUT', '/api/v1/books/1?mode=full').respond(200, { id: 1, title: "The Jungle 2.0" })
503 | book.title = "The Jungle 2.0"
504 | book.$save
505 | params: {mode: "full"}
506 | $httpBackend.flush()
507 | book.$promise.then jasmine.createSpy('success')
508 | $rootScope.$digest()
509 | expect(book.title).toEqual("The Jungle 2.0")
510 | return
511 | return
512 |
513 | return
514 |
--------------------------------------------------------------------------------
/test/basic-fields-spec.coffee:
--------------------------------------------------------------------------------
1 | describe "ORM basic functionality:", ->
2 | $rootScope = undefined
3 | $httpBackend = undefined
4 | $q = undefined
5 | Resource = undefined
6 | Book = undefined
7 |
8 | beforeEach module("restOrm")
9 |
10 | beforeEach inject(($injector) ->
11 | $rootScope = $injector.get("$rootScope")
12 | $httpBackend = $injector.get("$httpBackend")
13 | $q = $injector.get("$q")
14 | Resource = $injector.get("Resource")
15 |
16 | class Book extends Resource
17 | @urlEndpoint: '/api/v1/books/'
18 | @fields:
19 | title: { default: "" }
20 | subtitle: { default: "" }
21 | author: { default: null }
22 | tags: { default: [] }
23 |
24 | $initialize: ->
25 | @abc = 42
26 | #return
27 | )
28 |
29 | describe "Resource constructor", ->
30 | it "creates new instance", ->
31 | book = new Book()
32 | expect(book).toBeDefined()
33 | expect(book instanceof Book).toBeTruthy()
34 | return
35 | it "calls instance $initialize", ->
36 | spyOn(Book.prototype, '$initialize').andCallThrough()
37 | book = new Book()
38 | expect(Book.prototype.$initialize).toHaveBeenCalled()
39 | expect(book.abc).toBeDefined()
40 | expect(book.abc).toEqual(42)
41 | return
42 | return
43 | it "should return a properfly formed instance", ->
44 | book = new Book()
45 | expect(book.$meta).toBeDefined()
46 | expect(book.$meta.persisted).toBeFalsy()
47 | expect(book.$promise.then).toBeDefined()
48 | expect(book.$id).toBeDefined()
49 | expect(book.$id).toEqual(null)
50 | return
51 | it "should have defaults", ->
52 | book = new Book()
53 | expect(book.title).toEqual("")
54 | expect(book.subtitle).toEqual("")
55 | expect(book.author).toEqual(null)
56 | expect(book.tags).toEqual([])
57 | return
58 |
59 | return
60 |
61 | describe "Resource.Create()", ->
62 | it "returns new instance", ->
63 | book = Book.Create()
64 | expect(book).toBeDefined()
65 | expect(book instanceof Book).toBeTruthy()
66 | return
67 | it "returns new instance with defaults", ->
68 | book = Book.Create()
69 | expect(book.title).toEqual("")
70 | expect(book.subtitle).toEqual("")
71 | expect(book.author).toEqual(null)
72 | expect(book.tags).toEqual([])
73 | return
74 | it "returns new instance with passed values", ->
75 | book = Book.Create({ 'title': "Moby Dick" })
76 | expect(book.title).toEqual("Moby Dick")
77 | return
78 | it "returns new instance with defaults when incomplete values are given", ->
79 | book = Book.Create({ 'title': "Moby Dick" })
80 | expect(book.title).toEqual("Moby Dick")
81 | expect(book.subtitle).toEqual("")
82 | expect(book.author).toEqual(null)
83 | expect(book.tags).toEqual([])
84 | return
85 | it "uses correct REST endpoint", ->
86 | book = Book.Create({ 'title': "Moby Dick" })
87 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle", subtitle: "", author: null, tags: [] } )
88 | $httpBackend.flush()
89 | return
90 | it "fulfills instance $promiseDirect", ->
91 | book = Book.Create({ 'title': "Moby Dick" })
92 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle", subtitle: "", author: null, tags: [] } )
93 | $httpBackend.flush()
94 | handler = jasmine.createSpy('success')
95 | book.$promiseDirect.then handler
96 | $rootScope.$digest()
97 | expect(handler).toHaveBeenCalled()
98 | return
99 | it "fulfills instance $promise", ->
100 | book = Book.Create({ 'title': "Moby Dick" })
101 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle", subtitle: "", author: null, tags: [] } )
102 | $httpBackend.flush()
103 | handler = jasmine.createSpy('success')
104 | book.$promise.then handler
105 | $rootScope.$digest()
106 | expect(handler).toHaveBeenCalled()
107 | return
108 | it "creates an instance with an id", ->
109 | book = Book.Create({ 'title': "Moby Dick" })
110 | expect(book.$id).toEqual(null)
111 | expect(book.$meta.persisted).toBeFalsy()
112 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle", subtitle: "", author: null, tags: [] } )
113 | $httpBackend.flush()
114 | book.$promise.then jasmine.createSpy('success')
115 | $rootScope.$digest()
116 | expect(book.$meta.persisted).toBeTruthy()
117 | expect(book.$id).toEqual(1)
118 | return
119 | it "instance $promise result is the instance itself", ->
120 | book = Book.Create({ 'title': "Moby Dick" })
121 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle", subtitle: "", author: null, tags: [] } )
122 | $httpBackend.flush()
123 | handler = jasmine.createSpy('success')
124 | book.$promise.then handler
125 | $rootScope.$digest()
126 | result = handler.mostRecentCall.args[0]
127 | expect(result).toBe(book)
128 | return
129 | it "should handle params", ->
130 | $httpBackend.expect('POST', '/api/v1/books/?mode=full').respond(200, { id: 1, title: "Moby Dick" })
131 | book = Book.Create { 'title': "Moby Dick" },
132 | params: {mode: "full"}
133 | $httpBackend.flush()
134 | book.$promise.then jasmine.createSpy('success')
135 | $rootScope.$digest()
136 | expect(book.title).toEqual("Moby Dick")
137 | return
138 |
139 | return
140 |
141 | describe "Resource.Get()", ->
142 | it "should return a proper model instance", ->
143 | book = Book.Get(1)
144 | expect(book).toBeDefined()
145 | expect(book instanceof Book).toBeTruthy()
146 | expect(book.$meta).toBeDefined()
147 | expect(book.$promise.then).toBeDefined()
148 | expect(book.$promiseDirect.then).toBeDefined()
149 | return
150 | it "uses correct REST endpoint", ->
151 | $httpBackend.expect('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle", subtitle: "" })
152 | book = Book.Get(1)
153 | $httpBackend.flush()
154 | return
155 | it "fulfills instance $promiseDirect", ->
156 | $httpBackend.expect('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle", subtitle: "", author: null, tags: [] } )
157 | book = Book.Get(1)
158 | $httpBackend.flush()
159 | handler = jasmine.createSpy('success')
160 | book.$promiseDirect.then handler
161 | $rootScope.$digest()
162 | expect(handler).toHaveBeenCalled()
163 | return
164 | it "fulfills instance $promise", ->
165 | book = Book.Get(1)
166 | $httpBackend.when('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle", subtitle: "", author: null, tags: [] } )
167 | $httpBackend.flush()
168 | handler = jasmine.createSpy('success')
169 | book.$promise.then handler
170 | $rootScope.$digest()
171 | expect(handler).toHaveBeenCalled()
172 | return
173 | it "returns an instance with an id", ->
174 | book = Book.Get(1)
175 | $httpBackend.when('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle" })
176 | $httpBackend.flush()
177 | book.$promise.then jasmine.createSpy('success')
178 | $rootScope.$digest()
179 | expect(book.$id).toEqual(1)
180 | return
181 | it "returns instance with values from server", ->
182 | $httpBackend.expect('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle" })
183 | book = Book.Get(1)
184 | $httpBackend.flush()
185 | book.$promise.then jasmine.createSpy('success')
186 | $rootScope.$digest()
187 | expect(book.id).toBeDefined()
188 | expect(book.id).toEqual(1)
189 | expect(book.title).toEqual("The Jungle")
190 | return
191 | it "returns instance with defaults where not provided by server", ->
192 | book = Book.Get(1)
193 | $httpBackend.when('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle" })
194 | $httpBackend.flush()
195 | book.$promise.then jasmine.createSpy('success')
196 | $rootScope.$digest()
197 | expect(book.subtitle).toBeDefined()
198 | expect(book.subtitle).toEqual("")
199 | expect(book.author).toEqual(null)
200 | expect(book.tags).toEqual([])
201 | return
202 | it "should handle params", ->
203 | $httpBackend.expect('GET', '/api/v1/books/1?mode=full', (data) ->
204 | return (data == "{}")
205 | ).respond(200, { id: 1, title: "The Jungle" })
206 | book = Book.Get 1,
207 | params: {mode: "full"}
208 | $httpBackend.flush()
209 | book.$promise.then jasmine.createSpy('success')
210 | $rootScope.$digest()
211 | expect(book.title).toEqual("The Jungle")
212 | return
213 | it "should handle data", ->
214 | $httpBackend.expect('GET', '/api/v1/books/1', (data) ->
215 | return false if not (data and angular.isString(data))
216 | data = JSON.parse(data)
217 | return data and data.mode? and (data.mode == "full")
218 | ).respond(200, { id: 1, title: "The Jungle" })
219 | book = Book.Get 1,
220 | data: {mode: "full"}
221 | $httpBackend.flush()
222 | book.$promise.then jasmine.createSpy('success')
223 | $rootScope.$digest()
224 | expect(book.title).toEqual("The Jungle")
225 | return
226 |
227 | return
228 |
229 | describe "Resource.All()", ->
230 | it "should return a proper collection", ->
231 | collection = Book.All()
232 | expect(collection).toBeDefined()
233 | expect(angular.isArray(collection)).toBeTruthy()
234 | expect(collection.$meta).toBeDefined()
235 | expect(collection.$promise.then).toBeDefined()
236 | expect(collection.$promiseDirect.then).toBeDefined()
237 | return
238 | it "uses correct REST endpoint", ->
239 | collection = Book.All()
240 | $httpBackend.when('GET', '/api/v1/books/').respond(200, [])
241 | $httpBackend.flush()
242 | return
243 | it "fulfills collection $promiseDirect", ->
244 | collection = Book.All()
245 | $httpBackend.when('GET', '/api/v1/books/').respond(200, [ { id: 1, title: "The Jungle", subtitle: "" }, { id: 2, title: "Robinson Crusoe" } ])
246 | $httpBackend.flush()
247 | handler = jasmine.createSpy('success')
248 | collection.$promiseDirect.then handler
249 | $rootScope.$digest()
250 | expect(handler).toHaveBeenCalled()
251 | return
252 | it "fulfills collection $promise", ->
253 | collection = Book.All()
254 | $httpBackend.when('GET', '/api/v1/books/').respond(200, [ { id: 1, title: "The Jungle", subtitle: "" }, { id: 2, title: "Robinson Crusoe" } ])
255 | $httpBackend.flush()
256 | handler = jasmine.createSpy('success')
257 | collection.$promise.then handler
258 | $rootScope.$digest()
259 | expect(handler).toHaveBeenCalled()
260 | return
261 | it "collection $promise result is the collection itself", ->
262 | collection = Book.All()
263 | $httpBackend.when('GET', '/api/v1/books/').respond(200, [ { id: 1, title: "The Jungle", subtitle: "" }, { id: 2, title: "Robinson Crusoe" } ])
264 | $httpBackend.flush()
265 | handler = jasmine.createSpy('success')
266 | collection.$promise.then handler
267 | $rootScope.$digest()
268 | result = handler.mostRecentCall.args[0]
269 | expect(result).toBe(collection)
270 | return
271 | it "fulfills every instance $promiseDirect", ->
272 | collection = Book.All()
273 | $httpBackend.when('GET', '/api/v1/books/').respond(200, [ { id: 1, title: "The Jungle", subtitle: "" }, { id: 2, title: "Robinson Crusoe" } ])
274 | $httpBackend.flush()
275 | collection.$promise.then jasmine.createSpy('success')
276 | $rootScope.$digest()
277 | for instance in collection
278 | handler = jasmine.createSpy('success')
279 | instance.$promiseDirect.then handler
280 | $rootScope.$digest()
281 | expect(handler).toHaveBeenCalled()
282 | return
283 | it "fulfills every instance $promise", ->
284 | collection = Book.All()
285 | $httpBackend.when('GET', '/api/v1/books/').respond(200, [ { id: 1, title: "The Jungle", subtitle: "" }, { id: 2, title: "Robinson Crusoe" } ])
286 | $httpBackend.flush()
287 | collection.$promise.then jasmine.createSpy('success')
288 | $rootScope.$digest()
289 | for instance in collection
290 | handler = jasmine.createSpy('success')
291 | instance.$promise.then handler
292 | $rootScope.$digest()
293 | expect(handler).toHaveBeenCalled()
294 | return
295 | it "should return a collection of instances", ->
296 | collection = Book.All()
297 | $httpBackend.when('GET', '/api/v1/books/').respond(200, [ { id: 1, title: "The Jungle", subtitle: "" }, { id: 2, title: "Robinson Crusoe" } ])
298 | $httpBackend.flush()
299 | collection.$promise.then jasmine.createSpy('success')
300 | $rootScope.$digest()
301 | expect(collection.length).toEqual(2)
302 | expect(collection[0] instanceof Book).toBeTruthy()
303 | expect(collection[0].id).toEqual(1)
304 | expect(collection[0].$id).toBeDefined()
305 | expect(collection[0].$id).toEqual(1)
306 | expect(collection[0].title).toEqual("The Jungle")
307 | expect(collection[1] instanceof Book).toBeTruthy()
308 | expect(collection[1].id).toEqual(2)
309 | expect(collection[1].$id).toBeDefined()
310 | expect(collection[1].$id).toEqual(2)
311 | expect(collection[1].title).toEqual("Robinson Crusoe")
312 | return
313 | it "should handle params", ->
314 | $httpBackend.expect('GET', '/api/v1/books/?title=Robinson+Crusoe', (data) ->
315 | return (data == "{}")
316 | ).respond(200, [ { id: 2, title: "Robinson Crusoe" } ])
317 | collection = Book.All
318 | params: {title: "Robinson Crusoe"}
319 | $httpBackend.flush()
320 | collection.$promise.then jasmine.createSpy('success')
321 | $rootScope.$digest()
322 | expect(collection.length).toEqual(1)
323 | return
324 | it "should handle data", ->
325 | $httpBackend.expect('GET', '/api/v1/books/', (data) ->
326 | return false if not (data and angular.isString(data))
327 | data = JSON.parse(data)
328 | return data and data.title? and (data.title == "Robinson Crusoe")
329 | ).respond(200, [ { id: 2, title: "Robinson Crusoe" } ])
330 | collection = Book.All
331 | data: {title: "Robinson Crusoe"}
332 | $httpBackend.flush()
333 | collection.$promise.then jasmine.createSpy('success')
334 | $rootScope.$digest()
335 | expect(collection.length).toEqual(1)
336 | return
337 |
338 | return
339 |
340 | describe "instance .$save()", ->
341 | it "should return the instance itself", ->
342 | book = new Book()
343 | result = book.$save()
344 | expect(result).toBeDefined()
345 | expect(result instanceof Book).toBeTruthy()
346 | expect(result).toBe(book)
347 | return
348 | it "uses correct REST endpoint for fresh instance", ->
349 | $httpBackend.expect('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle" })
350 | book = new Book({ title: "The Jungle" })
351 | book.$save()
352 | $httpBackend.flush()
353 | return
354 | it "uses correct REST endpoint for existing instance", ->
355 | $httpBackend.expect('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle" })
356 | book = Book.Get(1)
357 | $httpBackend.flush()
358 | book.$promise.then jasmine.createSpy('success')
359 | $rootScope.$digest()
360 | $httpBackend.expect('PUT', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle 2.0" })
361 | book.title = "The Jungle 2.0"
362 | book.$save()
363 | $httpBackend.flush()
364 | return
365 | it "fulfills $promiseDirect for fresh instance", ->
366 | book = new Book({ title: "The Jungle" })
367 | book.$save()
368 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle" })
369 | $httpBackend.flush()
370 | handler = jasmine.createSpy('success')
371 | book.$promiseDirect.then handler
372 | $rootScope.$digest()
373 | expect(handler).toHaveBeenCalled()
374 | return
375 | it "fulfills $promise for fresh instance", ->
376 | book = new Book({ title: "The Jungle" })
377 | book.$save()
378 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle" })
379 | $httpBackend.flush()
380 | handler = jasmine.createSpy('success')
381 | book.$promise.then handler
382 | $rootScope.$digest()
383 | expect(handler).toHaveBeenCalled()
384 | return
385 | it "fulfills $promiseDirect for existing instance", ->
386 | book = Book.Get(1)
387 | $httpBackend.when('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle" })
388 | $httpBackend.flush()
389 | book.$promiseDirect.then jasmine.createSpy('success')
390 | $rootScope.$digest()
391 | book.title = "The Jungle 2.0"
392 | book.$save()
393 | $httpBackend.when('PUT', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle 2.0" })
394 | $httpBackend.flush()
395 | handler = jasmine.createSpy('success')
396 | book.$promiseDirect.then handler
397 | $rootScope.$digest()
398 | expect(handler).toHaveBeenCalled()
399 | return
400 | it "fulfills $promise for existing instance", ->
401 | book = Book.Get(1)
402 | $httpBackend.when('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle" })
403 | $httpBackend.flush()
404 | book.$promise.then jasmine.createSpy('success')
405 | $rootScope.$digest()
406 | book.title = "The Jungle 2.0"
407 | book.$save()
408 | $httpBackend.when('PUT', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle 2.0" })
409 | $httpBackend.flush()
410 | handler = jasmine.createSpy('success')
411 | book.$promise.then handler
412 | $rootScope.$digest()
413 | expect(handler).toHaveBeenCalled()
414 | return
415 | it "fulfills new $promiseDirect", ->
416 | book = new Book({ title: "The Jungle" })
417 | book.$save()
418 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle" })
419 | $httpBackend.flush()
420 | handler = jasmine.createSpy('success')
421 | book.$promiseDirect.then handler
422 | $rootScope.$digest()
423 | expect(handler).toHaveBeenCalled()
424 | book.title = "The Jungle 2.0"
425 | book.$save()
426 | $httpBackend.when('PUT', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle 2.0" })
427 | $httpBackend.flush()
428 | handler = jasmine.createSpy('success')
429 | book.$promiseDirect.then handler
430 | $rootScope.$digest()
431 | expect(handler).toHaveBeenCalled()
432 | return
433 | it "fulfills new $promise", ->
434 | book = new Book({ title: "The Jungle" })
435 | book.$save()
436 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle" })
437 | $httpBackend.flush()
438 | handler = jasmine.createSpy('success')
439 | book.$promise.then handler
440 | $rootScope.$digest()
441 | expect(handler).toHaveBeenCalled()
442 | book.title = "The Jungle 2.0"
443 | book.$save()
444 | $httpBackend.when('PUT', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle 2.0" })
445 | $httpBackend.flush()
446 | handler = jasmine.createSpy('success')
447 | book.$promise.then handler
448 | $rootScope.$digest()
449 | expect(handler).toHaveBeenCalled()
450 | return
451 | it "provides an id to a fresh instance", ->
452 | book = new Book({ title: "The Jungle" })
453 | book.$save()
454 | $httpBackend.when('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle" })
455 | $httpBackend.flush()
456 | book.$promise.then jasmine.createSpy('success')
457 | $rootScope.$digest()
458 | expect(book.$id).toBeDefined()
459 | expect(book.$id).toEqual(1)
460 | return
461 | it "saves fresh instance", ->
462 | $httpBackend.expect('POST', '/api/v1/books/').respond(200, { id: 1, title: "The Jungle" })
463 | book = new Book({ title: "The Jungle" })
464 | book.$save()
465 | $httpBackend.flush()
466 | book.$promise.then jasmine.createSpy('success')
467 | $rootScope.$digest()
468 | expect(book.id).toBeDefined()
469 | expect(book.id).toEqual(1)
470 | expect(book.title).toEqual("The Jungle")
471 | return
472 | it "saves changed values of existing instance", ->
473 | book = Book.Get(1)
474 | $httpBackend.when('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle" })
475 | $httpBackend.flush()
476 | book.$promise.then jasmine.createSpy('success')
477 | $rootScope.$digest()
478 | book.title = "The Jungle 2.0"
479 | book.$save()
480 | $httpBackend.when('PUT', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle 2.0" })
481 | $httpBackend.flush()
482 | book.$promise.then jasmine.createSpy('success')
483 | $rootScope.$digest()
484 | expect(book.title).toEqual("The Jungle 2.0")
485 | return
486 | it "should handle params for fresh instance", ->
487 | $httpBackend.expect('POST', '/api/v1/books/?mode=full').respond(200, { id: 1, title: "The Jungle" })
488 | book = new Book({ title: "The Jungle" })
489 | book.$save
490 | params: {mode: "full"}
491 | $httpBackend.flush()
492 | book.$promise.then jasmine.createSpy('success')
493 | $rootScope.$digest()
494 | expect(book.title).toEqual("The Jungle")
495 | return
496 | it "should handle params for existing instance", ->
497 | book = Book.Get(1)
498 | $httpBackend.when('GET', '/api/v1/books/1').respond(200, { id: 1, title: "The Jungle" })
499 | $httpBackend.flush()
500 | book.$promise.then jasmine.createSpy('success')
501 | $rootScope.$digest()
502 | $httpBackend.expect('PUT', '/api/v1/books/1?mode=full').respond(200, { id: 1, title: "The Jungle 2.0" })
503 | book.title = "The Jungle 2.0"
504 | book.$save
505 | params: {mode: "full"}
506 | $httpBackend.flush()
507 | book.$promise.then jasmine.createSpy('success')
508 | $rootScope.$digest()
509 | expect(book.title).toEqual("The Jungle 2.0")
510 | return
511 | return
512 |
513 | return
514 |
--------------------------------------------------------------------------------
/src/ORM.coffee:
--------------------------------------------------------------------------------
1 | angular.module("restOrm", [
2 | ]).factory("Resource", ($http, $q, $rootScope) ->
3 |
4 | # based on http://stackoverflow.com/a/4994244
5 | isEmpty = (obj) ->
6 | # null and undefined are "empty"
7 | return true unless obj?
8 |
9 | # Assume if it has a length property with a non-zero value
10 | # that that property is correct.
11 | return false if obj.length > 0
12 | return true if obj.length is 0
13 |
14 | # Otherwise, does it have any properties of its own?
15 | # Note that this doesn't handle
16 | # toString and valueOf enumeration bugs in IE < 9
17 | for own key of obj
18 | return false
19 | true
20 |
21 | startsWith = (s, sub) -> s.slice(0, sub.length) == sub
22 | endsWith = (s, sub) -> sub == '' or s.slice(-sub.length) == sub
23 |
24 | isKeyLike = (value) ->
25 | if not value?
26 | return false
27 | if angular.isUndefined(value) or (value == null)
28 | return false
29 | if angular.isObject(value) or angular.isArray(value)
30 | return false
31 | return true
32 |
33 | # based on http://stackoverflow.com/a/2676231
34 | _urljoin = (components...) ->
35 | normalize = (str) ->
36 | str.replace(/[\/]+/g, "/").replace(/\/\?/g, "?").replace(/\/\#/g, "#").replace /\:\//g, "://"
37 | url_result = []
38 |
39 | if components and (components.length > 0)
40 | skip = 0
41 | while (skip < components.length)
42 | if components[skip]
43 | break
44 | ++skip
45 | components = components[skip..] if skip
46 |
47 | last_comp = null
48 | for component in components
49 | continue if not (component? and component)
50 | last_comp = component
51 | c_url = "#{component}".split("/")
52 | for i in [0...c_url.length]
53 | if c_url[i] is ".."
54 | url_result.pop()
55 | else if (c_url[i] is ".") or (c_url[i] is "")
56 | continue
57 | else
58 | url_result.push c_url[i]
59 |
60 | r = normalize url_result.join("/")
61 | if components and (components.length >= 1)
62 | component = "#{components[0]}"
63 | if startsWith(component, "//")
64 | r = "//" + r
65 | else if startsWith(component, "/")
66 | r = "/" + r
67 |
68 | last_comp = "#{last_comp}"
69 | if endsWith(last_comp, "/") and (not endsWith(r, "/"))
70 | r = r + "/"
71 | r
72 |
73 | urljoin = ->
74 | encodeURI _urljoin arguments...
75 |
76 | ###*
77 | # @ngdoc service
78 | # @name restOrm.Resource
79 | #
80 | # @description
81 | # # Resource
82 | # Is the base class for RESTful resource models.
83 | #
84 | # Derive from `Resource` providing proper values for class properties
85 | # as described below to define models for your resources.
86 | #
87 | # A very minimal example in JavaScript:
88 | #
89 | # ```javascript
90 | # var Book = Resource.Subclass({}, {
91 | # urlEndpoint: '/api/books/',
92 | # idField: '_id'
93 | # });
94 | # ```
95 | #
96 | # of in CoffeeScript:
97 | #
98 | # ```coffeescript
99 | # class Book extends Resource
100 | # @.urlEndpoint: '/api/books/'
101 | # @.idField: '_id'
102 | # ```
103 | # (the `.` has been added to circumvent an `ngdoc` bug regarding parsing of `@` in blockquotes...)
104 | #
105 | # @returns {object} Resource class
106 | ###
107 | class Resource
108 | ###*
109 | # @ngdoc property {String}
110 | # @name .#urlPrefix
111 | # @propertyOf restOrm.Resource
112 | #
113 | # @description
114 | # # urlPrefix
115 | # **class property** - prefix that will be prepended to all URLs
116 | # for this resource.
117 | # Defaults to the empty string (in this case, nothing will be prepended).
118 | #
119 | # The final base URL will have the form
120 | #
121 | # `urlPrefix` / `urlEndpoint`
122 | #
123 | # (Note that slashes will be added only where necessary)
124 | #
125 | # This property is intended to be specified on subclasses of `Resource`.
126 | ###
127 | @urlPrefix: ''
128 |
129 | ###*
130 | # @ngdoc property {String}
131 | # @name .#urlEndpoint
132 | # @propertyOf restOrm.Resource
133 | #
134 | # @description
135 | # # urlEndpoint
136 | # **class property** - the base URL for the resource.
137 | #
138 | # The final base URL will have the form
139 | #
140 | # `urlPrefix` / `urlEndpoint`
141 | #
142 | # (Note that slashes will be added only where necessary)
143 | #
144 | # This property is intended to be specified on subclasses of `Resource`.
145 | ###
146 | @urlEndpoint: ''
147 |
148 | ###*
149 | # @ngdoc property {String}
150 | # @name .#idField
151 | # @propertyOf restOrm.Resource
152 | #
153 | # @description
154 | # # idField
155 | # **class property** - (optional) the name of the field containing the ID of the resource in
156 | # remote endpoint responses.
157 | #
158 | # Defaults to `id`.
159 | #
160 | # This property is intended to be specified on subclasses of `Resource`.
161 | ###
162 | @idField: 'id'
163 |
164 | ###*
165 | # @ngdoc property {Object}
166 | # @name .#fields
167 | # @propertyOf restOrm.Resource
168 | #
169 | # @description
170 | # # fields
171 | # **class property** - (optional) object specifying names and kinds of resource fields.
172 | #
173 | # It's possible to specify an entry for each field, with this form:
174 | #
175 | # ```javascript
176 | # {
177 | # ...
178 | # NAME: {
179 | # default: DEFAULT_VALUE,
180 | # remote: REMOTE_FIELD_NAME,
181 | # type: FIELD_TYPE,
182 | # model: RELATED_MODEL
183 | # },
184 | # ...
185 | # }
186 | # ```
187 | #
188 | # where:
189 | #
190 | # - *NAME* is the field name as used on the resource (model instance). This can be
191 | # different from the remote endpoint field name.
192 | # - *DEFAULT_VALUE* is the default value for the field, used if the remote doesn't
193 | # provide a value or when creating a resource without specifying all fields
194 | # - *REMOTE_FIELD_NAME* is the (optional) field name on the remote endpoint. If not
195 | # specified, it's assumed to be the same as *NAME*
196 | # - *FIELD_TYPE* at this time is used only to specify relations. If specified can
197 | # be `Resource.Reference` or `Resource.ManyToMany`
198 | # - *RELATED_MODEL* used only for relations, specifies the related model (must be a
199 | # `Resource` subclass)
200 | #
201 | # All of the entry object fields are optional.
202 | #
203 | #
204 | # If `fields` is not specified, fields will be fetched and copied between
205 | # responses and resource models as-is.
206 | #
207 | # This property is intended to be specified on subclasses of `Resource`.
208 | ###
209 | @fields = {}
210 |
211 | ###*
212 | # @ngdoc property {Object}
213 | # @name .#defaults
214 | # @propertyOf restOrm.Resource
215 | #
216 | # @description
217 | # # defaults
218 | # **class property** - (optional) object specifying default values for resource fields.
219 | # It is meant to be an easy shortcut for those cases where the `fields` complexity is
220 | # not needed.
221 | #
222 | # This property is intended to be specified on subclasses of `Resource`.
223 | ###
224 | @defaults: {}
225 |
226 | @headers: {}
227 |
228 | @prepareRequest: null
229 | @transformResponse: null
230 | @postResponse: null
231 |
232 | @Reference: 'reference' # many-to-one
233 | @ManyToMany: 'many2many' # many-to-many
234 |
235 | @include: (obj) ->
236 | throw new Error('include(obj) requires obj') unless obj
237 | for key, value of obj when key not in ['included', 'extended']
238 | @::[key] = value
239 | obj.included?.apply(this)
240 | this
241 |
242 | @extend: (obj) ->
243 | throw new Error('extend(obj) requires obj') unless obj
244 | for key, value of obj when key not in ['included', 'extended']
245 | @[key] = value
246 | obj.extended?.apply(this)
247 | this
248 |
249 | ###*
250 | # @ngdoc method
251 | # @name Resource#Subclass
252 | # @methodOf restOrm.Resource
253 | #
254 | # @description
255 | # # Resource.Subclass()
256 | # **class method** that returns a new subclass derived from `Resource`
257 | # extended with the specified instance and class properties.
258 | #
259 | # This method is intended to be used from plain *JavaScript*.
260 | #
261 | # *CoffeeScript* users should rely on the native `class ... extends ...` syntax
262 | # to create `Resource` subclasses.
263 | #
264 | # @param {object} instances Properties to add to instances of the newly created class
265 | #
266 | # @param {object} statics Class properties of the newly created class
267 | #
268 | # @returns {function} the new class (a constructor function)
269 | ###
270 | @Subclass: (instances, statics) ->
271 | class Result extends this
272 | Result.include(instances) if instances
273 | Result.extend(statics) if statics
274 | Result::$super = (method) -> @constructor.__super__[method]
275 | Result
276 |
277 | ###*
278 | # @ngdoc method
279 | # @name Resource
280 | # @methodOf restOrm.Resource
281 | #
282 | # @description
283 | # # Resource()
284 | # **constructor** for `Resource`
285 | #
286 | # Usually one would never call this constructor direcly, but always through subclasses.
287 | #
288 | # @param {object|null=} data Object that will be used to initialize the resource (model instance)
289 | #
290 | # @param {object=} opts Options
291 | ###
292 | constructor: (data=null, opts={}) ->
293 | @$meta =
294 | persisted: false
295 | async:
296 | direct:
297 | deferred: null
298 | resolved: true # signal we need to create it
299 | m2o:
300 | deferred: null
301 | resolved: true # signal we need to create it
302 | m2m:
303 | deferred: null
304 | resolved: true # signal we need to create it
305 | angular.extend(@$meta, opts)
306 | @$error = null
307 |
308 | @$id = null
309 | @_fromObject(data or {})
310 |
311 | # @$promise is a promise fulfilled when the object is completely fetched, complete
312 | # with relations (reference and m2m objects).
313 | # @$promiseDirect is fulfilled when the object is fetched (without caring for relations)
314 | @$promise = null
315 | @$promiseDirect = null
316 |
317 | @_setupPromises()
318 | @_fetchRelations()
319 |
320 | @$initialize?(arguments...)
321 |
322 | ###*
323 | # @ngdoc method
324 | # @name Resource#Create
325 | # @methodOf restOrm.Resource
326 | #
327 | # @description
328 | # # Resource.Create()
329 | # **class method ** - creates a model resource (instance) and persists it on the remote side.
330 | #
331 | # Usually this method will be called on a `Resource` subclass.
332 | #
333 | # @param {data|null=} data Object used to initialize the new model instance properties
334 | #
335 | # @param {object=} opts Options passed to `$save()`
336 | #
337 | # @returns {object} newly created resource (model instance)
338 | ###
339 | @Create: (data=null, opts={}) ->
340 | data = data or @defaults
341 | item = new @(data, {persisted: false})
342 | item.$save(opts)
343 | item
344 |
345 | ###*
346 | # @ngdoc method
347 | # @name Resource#Get
348 | # @methodOf restOrm.Resource
349 | #
350 | # @description
351 | # # Resource.Get
352 | # **class method ** - fetches and returns a resource with the given id.
353 | #
354 | # A model instance for the resource is constructed and returned, and it
355 | # will be populated with the contents fetched from the remote endpoint
356 | # for the resource with the specified `id`.
357 | #
358 | # The HTTP fetch will be performed asynchronously, so the model instance,
359 | # even if returned immediately, will be populated in an
360 | # incremental fashion.
361 | #
362 | # To allow the user to be notified on the completion of the fetch process,
363 | # the model instance contains as special properties a couple of promises that
364 | # will be fulfilled when the fetch completes.
365 | #
366 | # The first one is named `$promise`. This will be fulfilled when
367 | # the resource will have been fetched **along with all its relations**
368 | # (and the relations of the relations... down to the deepest nesting levels).
369 | #
370 | # The other one is named `$promiseDirect`. This will be fulfilled when the
371 | # resource will have been fetched, but ignoring relations.
372 | #
373 | # The method will cause an http request of the form:
374 | #
375 | # `GET` *RESOURCE_URL* / *id*
376 | #
377 | # Usually this method will be called on a `Resource` subclass.
378 | #
379 | # @param {Number|String} id Remote endpoint id of the resource to fetch
380 | #
381 | # @param {object=} opts Options object containing optional `params` and `data`
382 | # fields passed to the respective counterparts of the `$http` call
383 | #
384 | # @returns {object} fetched resource (model instance)
385 | ###
386 | @Get: (id, opts={}) ->
387 | item = new @()
388 | url = urljoin @_GetURLBase(), id
389 | item._setupPromises()
390 | req = @_PrepareRequest {
391 | id: id
392 | opts: opts
393 | what: 'Get'
394 | method: 'GET'
395 | url: url
396 | headers: @_BuildHeaders 'Get', 'GET', null
397 | params: opts.params or {}
398 | data: opts.data or {}
399 | item: item
400 | }
401 | $http(
402 | method: req.method
403 | url: req.url
404 | headers: req.headers
405 | params: req.params
406 | data: req.data
407 | ).then ((response) =>
408 | res = @_TransformResponse req, response
409 | item._fromRemote(res.data)
410 | @_PostResponse res
411 | ), (response) ->
412 | item.$error = response
413 | item.resolvePromise(item.$meta.async.direct.deferred, false)
414 | item.resolvePromise(item.$meta.async.m2o.deferred, false)
415 | item.resolvePromise(item.$meta.async.m2m.deferred, false)
416 |
417 | item
418 |
419 | ###*
420 | # @ngdoc method
421 | # @name Resource#All
422 | # @methodOf restOrm.Resource
423 | #
424 | # @description
425 | # # Resource.All
426 | # **class method ** - fetches and returns all the remote resources.
427 | #
428 | # This method returns a *collection*, an Array augmented with some
429 | # additional properties, as detailed below.
430 | #
431 | # The HTTP fetch will be performed asynchronously, so the collection,
432 | # even if returned immediately, will be filled with results in an
433 | # incremental fashion.
434 | #
435 | # To allow the user to be notified on the completion of the fetch process,
436 | # the collection contains as special properties a couple of promises that
437 | # will be fulfilled when the fetch completes.
438 | #
439 | # The first one is named `$promise`. This will be fulfilled when all
440 | # collection items will have been fetched **along with all their relations**
441 | # (and the relations of the relations... down to the deepest nesting levels).
442 | #
443 | # The other one is named `$promiseDirect`. This will be fulfilled when all
444 | # collection items will have been fetched, but ignoring relations.
445 | #
446 | # The method will cause an http request of the form:
447 | #
448 | # `GET` *RESOURCE_URL* /
449 | #
450 | # Usually this method will be called on a `Resource` subclass.
451 | #
452 | # @param {object=} opts Options object containing optional `params` and `data`
453 | # fields passed to the respective counterparts of the `$http` call
454 | #
455 | # @returns {object} fetched collection (augmented array of model instances)
456 | ###
457 | @All: (opts={}) ->
458 | collection = @_MakeCollection()
459 | url = urljoin @_GetURLBase()
460 | req = @_PrepareRequest {
461 | opts: opts
462 | what: 'All'
463 | method: 'GET'
464 | url: url
465 | headers: @_BuildHeaders 'All', 'GET', null
466 | params: opts.params or {}
467 | data: opts.data or {}
468 | collection: collection
469 | }
470 | $http(
471 | method: req.method
472 | url: req.url
473 | headers: req.headers
474 | params: req.params
475 | data: req.data
476 | ).then ((response) =>
477 | res = @_TransformResponse req, response
478 | for values in res.data
479 | collection.push @_MakeInstanceFromRemote(values)
480 | @_PostResponse res
481 | collection.$finalize()
482 | ), (response) =>
483 | res = @_TransformResponse req, response
484 | @_PostResponse res
485 | collection.$finalize(false, response)
486 | collection
487 |
488 | @Search: (field, value, opts={}) ->
489 | collection = @_MakeCollection()
490 | url = urljoin @_GetURLBase(), "search", field, value
491 | req = @_PrepareRequest {
492 | field: field
493 | value: value
494 | opts: opts
495 | what: 'Search'
496 | method: 'GET'
497 | url: url
498 | headers: @_BuildHeaders 'Search', 'GET', null
499 | params: opts.params or {}
500 | data: opts.data or {}
501 | collection: collection
502 | }
503 | $http(
504 | method: req.method
505 | url: req.url
506 | headers: req.headers
507 | params: req.params
508 | data: req.data
509 | ).then ((response) =>
510 | res = @_TransformResponse req, response
511 | for values in res.data
512 | collection.push @_MakeInstanceFromRemote(values)
513 | @_PostResponse res
514 | # @_MakeInstanceFromRemote(values) for values in response.data
515 | collection.$finalize()
516 | ), (response) =>
517 | res = @_TransformResponse req, response
518 | @_PostResponse res
519 | collection.$finalize(false, response)
520 | collection
521 |
522 | ###*
523 | # @ngdoc method
524 | # @name Resource#$save
525 | # @methodOf restOrm.Resource
526 | #
527 | # @description
528 | # Saves the resource represented by this model instance.
529 | #
530 | # If the model instance represents a resource obtained from the
531 | # remote endpoint (thus having an ID), than the method will update
532 | # the remote resource (using an HTTP `PUT`), otherwise it will create
533 | # the resource on the remote endpoint (using an HTTP `POST`).
534 | #
535 | # The operation will update the model representation with the data
536 | # obtained from the remote endpoint.
537 | #
538 | # The HTTP operation will be performed asynchronously, so the `$promise`
539 | # and `$promiseDirect` promises will be re-generated to inform of the
540 | # completion.
541 | #
542 | # For a new resource, the method will cause an http request of the form:
543 | #
544 | # `POST` *RESOURCE_URL* /
545 | #
546 | # otherwise for an existing resource:
547 | #
548 | # `PUT` *RESOURCE_URL* / *id*
549 | #
550 | # Usually this method will be called on a `Resource` subclass.
551 | #
552 | # @param {object=} opts Options object containing optional `params` and `data`
553 | # fields passed to the respective counterparts of the `$http` call
554 | #
555 | # @returns {object} the model instance itself
556 | ###
557 | $save: (opts={}) ->
558 | data = @_toRemoteObject()
559 | if @$meta.persisted and @$id?
560 | method = 'PUT'
561 | url = urljoin @_getURLBase(), @$id
562 | else
563 | method = 'POST'
564 | if @constructor.idField of data
565 | delete data[@constructor.idField]
566 | url = urljoin @_getURLBase()
567 |
568 | # TODO: check deferred/promise re-setup
569 | @_setupPromises()
570 |
571 | req = @_prepareRequest {
572 | opts: opts
573 | what: '$save'
574 | method: method
575 | url: url
576 | headers: @_buildHeaders '$save', method
577 | params: opts.params or {}
578 | data: data
579 | cache: false
580 | item: @
581 | }
582 |
583 | $http(
584 | method: req.method
585 | url: req.url
586 | data: req.data
587 | cache: req.cache
588 | headers: req.headers
589 | params: req.params
590 | ).then ((response) =>
591 | res = @_transformResponse req, response
592 | @_fromRemote(res.data)
593 | @_postResponse res
594 | ), (response) =>
595 | @$error = response
596 | @resolvePromise(@$meta.async.direct.deferred, false)
597 | @resolvePromise(@$meta.async.m2o.deferred, false)
598 | @resolvePromise(@$meta.async.m2m.deferred, false)
599 | @
600 |
601 | # -----------------------------------------------------------------
602 | # PRIVATE METHODS
603 | # -----------------------------------------------------------------
604 |
605 | @_MakeCollection: ->
606 | collection = []
607 | collection.$useApplyAsync = false
608 | collection.$meta =
609 | model: @
610 | async:
611 | direct:
612 | deferred: $q.defer()
613 | resolved: false
614 | complete:
615 | deferred: $q.defer()
616 | resolved: false
617 | collection.$error = false
618 | collection.$promise = collection.$meta.async.complete.deferred.promise
619 | collection.$promiseDirect = collection.$meta.async.direct.deferred.promise
620 | collection.$getItemsPromises = ->
621 | (instance.$promise for instance in collection)
622 | collection.$getItemsPromiseDirects = ->
623 | (instance.$promiseDirect for instance in collection)
624 | collection.$_getPromiseForItems = ->
625 | $q.all collection.$getItemsPromises()
626 | collection.$_getPromiseDirectForItems = ->
627 | $q.all collection.$getItemsPromiseDirects()
628 | collection._resolvePromise = (deferred, success=true) ->
629 | if success
630 | return deferred.resolve(collection)
631 | return deferred.reject(collection)
632 | collection.resolvePromise = (deferred, success=true) ->
633 | if collection.$useApplyAsync
634 | $rootScope.$applyAsync ->
635 | collection._resolvePromise(deferred, success)
636 | else
637 | collection._resolvePromise(deferred, success)
638 | if not $rootScope.$$phase
639 | $rootScope.$apply()
640 | collection
641 | collection.$finalize = (success=true, response=null) ->
642 | items_success = success
643 | collection.$_getPromiseForItems().then ( ->
644 | #collection.$meta.async.complete.deferred.resolve(collection)
645 | collection.resolvePromise(collection.$meta.async.complete.deferred, success)
646 | collection.$meta.async.complete.resolved = true
647 | ), ->
648 | items_success = false
649 | #collection.$meta.async.complete.deferred.reject(collection)
650 | collection.resolvePromise(collection.$meta.async.complete.deferred, items_success)
651 | collection.$meta.async.complete.resolved = true
652 | collection.$error = true
653 | #collection.$meta.async.direct.deferred.resolve(collection)
654 | collection.resolvePromise(collection.$meta.async.direct.deferred, success)
655 | collection.$meta.async.direct.resolved = true
656 | if (not success) and response
657 | collection.$error = response
658 | else
659 | collection.$error = (not (success and items_success))
660 | collection
661 | collection
662 |
663 | @_MakeInstanceFromRemote: (data) ->
664 | instance = new @()
665 | instance._setupPromises()
666 | instance._fromRemote(data)
667 | instance
668 |
669 | @_GetURLBase: ->
670 | _urljoin @urlPrefix, @urlEndpoint
671 |
672 | @_BuildHeaders: (what=null, method=null, instance=null) ->
673 | if not @headers?
674 | return {}
675 | if angular.isFunction(@headers)
676 | return @headers.call(@, {klass: @, what: what, method: method, instance: instance})
677 | else if angular.isObject(@headers)
678 | processHeaderSource = (dst, src) =>
679 | for name, value of src
680 | if angular.isFunction(value)
681 | dst_value = value.call(@, {klass: @, what: what, method: method, instance: instance})
682 | else
683 | dst_value = value
684 | if dst_value != null
685 | dst[name] = dst_value
686 | dst
687 |
688 | dst_headers = {}
689 | if ('common' of @headers) or (what? and (what of @headers)) or (method? and (method of @headers))
690 | if 'common' of @headers
691 | processHeaderSource dst_headers, @headers.common
692 | if what? and (what of @headers)
693 | processHeaderSource dst_headers, @headers[what]
694 | if method? and (method of @headers)
695 | processHeaderSource dst_headers, @headers[method]
696 | else
697 | processHeaderSource dst_headers, @headers
698 | return dst_headers
699 | return {}
700 |
701 | @_PrepareRequest: (req) ->
702 | req.klass = @
703 | if @prepareRequest? and angular.isFunction(@prepareRequest)
704 | return @prepareRequest.call(@, req)
705 | return req
706 |
707 | _prepareRequest: (req) ->
708 | req.instance = @
709 | @constructor._PrepareRequest req
710 |
711 | @_TransformResponse: (req, http_response, instance=null) ->
712 | res = angular.extend {}, req, {
713 | request: req, response: http_response, data: http_response.data
714 | }
715 | res.klass = @
716 | res.instance = instance
717 | res.data = res.response.data
718 | res.status = res.response.status
719 | res.headers = res.response.headers
720 | res.config = res.response.config
721 | res.statusText = res.response.statusText
722 | if @transformResponse? and angular.isFunction(@transformResponse)
723 | return @transformResponse.call(@, res)
724 | return res
725 |
726 | _transformResponse: (req, http_response) ->
727 | @constructor._TransformResponse req, http_response, @
728 |
729 | @_PostResponse: (res) ->
730 | res.klass = @
731 | if @postResponse? and angular.isFunction(@postResponse)
732 | return @postResponse.call(@, res)
733 | return res
734 |
735 | _postResponse: (res) ->
736 | res.instance = @
737 | @constructor._PostResponse res
738 |
739 | _buildHeaders: (what=null, method=null) ->
740 | @constructor._BuildHeaders what, method, @
741 |
742 | _setupPromises: ->
743 | # @$promise is a promise fulfilled when the object is completely fetched, complete
744 | # with relations (reference and m2m objects).
745 | # @$promiseDirect is fulfilled when the object is fetched (without caring for relations)
746 |
747 | changed = false
748 | if @$meta.async.direct.resolved or (not @$meta.async.direct.deferred?)
749 | # console.log "#{@constructor.name}: creating new direct promise ($promiseDirect)"
750 | @$meta.async.direct.deferred = $q.defer()
751 | @$meta.async.direct.resolved = false
752 | changed = true
753 |
754 | @$promiseDirect = @$meta.async.direct.deferred.promise.then =>
755 | @$meta.async.direct.resolved = true
756 | # console.log "#{@constructor.name}: resolved $promiseDirect"
757 | return @
758 |
759 | if @$meta.async.m2o.resolved or (not @$meta.async.m2o.deferred?)
760 | # console.log "#{@constructor.name}: creating new m2o promise"
761 | @$meta.async.m2o.deferred = $q.defer()
762 | @$meta.async.m2o.resolved = false
763 | changed = true
764 |
765 | @$meta.async.m2o.deferred.promise.then =>
766 | @$meta.async.m2o.resolved = true
767 | # console.log "#{@constructor.name}: resolved m2o"
768 | return @
769 |
770 | if @$meta.async.m2m.resolved or (not @$meta.async.m2m.deferred?)
771 | # console.log "#{@constructor.name}: creating new m2m promise"
772 | @$meta.async.m2m.deferred = $q.defer()
773 | @$meta.async.m2m.resolved = false
774 | changed = true
775 |
776 | @$meta.async.m2m.deferred.promise.then =>
777 | @$meta.async.m2m.resolved = true
778 | # console.log "#{@constructor.name}: resolved m2m"
779 | return @
780 |
781 | if changed
782 | # console.log "#{@constructor.name}: creating new $promise"
783 | @$promise = $q.all([
784 | @$meta.async.direct.deferred.promise,
785 | @$meta.async.m2o.deferred.promise,
786 | @$meta.async.m2m.deferred.promise
787 | ]).then =>
788 | @$meta.async.direct.resolved = true
789 | @$meta.async.m2o.resolved = true
790 | @$meta.async.m2m.resolved = true
791 | # console.log "#{@constructor.name}: resolved everything ($promise)"
792 | return @
793 | @
794 |
795 | _getURLBase: ->
796 | _urljoin @constructor.urlPrefix, @constructor.urlEndpoint
797 |
798 | _fetchRelations: ->
799 | if @$id?
800 | @_fetchReferences()
801 | @_fetchM2M()
802 | @
803 |
804 | _fetchReferences: ->
805 | fetchReference = (instance, reference, promises) ->
806 | fieldName = reference.name
807 | if (fieldName of instance) and instance[fieldName]? and isKeyLike(instance[fieldName])
808 | ref_id = instance[fieldName]
809 | record = reference.model.Get(ref_id)
810 | instance[fieldName] = record
811 | promises.push record.$promise
812 | promises = []
813 | for name of @constructor.fields
814 | def = @_getField(name)
815 | if def.type is @constructor.Reference
816 | fetchReference(@, def, promises)
817 | $q.all(promises).then ( =>
818 | #@$meta.async.m2o.deferred.resolve(@)
819 | @resolvePromise(@$meta.async.m2o.deferred)
820 | ), =>
821 | @resolvePromise(@$meta.async.m2o.deferred, false)
822 | @
823 |
824 | _fetchM2M: ->
825 | fetchM2M = (instance, m2m, promises, collections) ->
826 | fieldName = m2m.name
827 | if (fieldName of instance) and instance[fieldName]? and angular.isArray(instance[fieldName])
828 | refs_promises = []
829 | refs_collection = m2m.model._MakeCollection()
830 | for ref_id in instance[fieldName]
831 | record = m2m.model.Get(ref_id)
832 | refs_collection.push record
833 | refs_promises.push record.$promise
834 | instance[fieldName] = refs_collection
835 | promises.push refs_collection.$promise
836 | collections.push refs_collection
837 | else
838 | instance[fieldName] = []
839 | promises = []
840 | collections = []
841 | for name of @constructor.fields
842 | def = @_getField(name)
843 | if def.type is @constructor.ManyToMany
844 | fetchM2M(@, def, promises, collections)
845 | $q.all(promises).then ( =>
846 | #@$meta.async.m2m.deferred.resolve(@)
847 | @resolvePromise(@$meta.async.m2m.deferred)
848 | ) , =>
849 | @resolvePromise(@$meta.async.m2m.deferred, false)
850 | for refs_collection in collections
851 | refs_collection.$finalize()
852 | @
853 |
854 | _fromRemote: (data) ->
855 | @_fromRemoteObject(data)
856 | @$meta.persisted = true
857 | #@$meta.async.direct.deferred.resolve(@)
858 | @resolvePromise(@$meta.async.direct.deferred)
859 | @_fetchRelations()
860 | return @
861 |
862 | _resolvePromise: (deferred, success=true) ->
863 | if success
864 | return deferred.resolve(@)
865 | return deferred.reject(@)
866 |
867 | resolvePromise: (deferred, success=true) ->
868 | if @$useApplyAsync
869 | $rootScope.$applyAsync =>
870 | @_resolvePromise(deferred, success)
871 | else
872 | @_resolvePromise(deferred, success)
873 | if not $rootScope.$$phase
874 | $rootScope.$apply()
875 | @
876 |
877 | _getFields: ->
878 | fieldsSpec = {}
879 |
880 | for name, value of @constructor.defaults
881 | fieldsSpec[name] = { default: value }
882 |
883 | # add the id field to class 'fields' property if not specified
884 | if not (@constructor.idField of fieldsSpec)
885 | fieldsSpec[@constructor.idField] = { default: null }
886 |
887 | angular.extend fieldsSpec, @constructor.fields
888 |
889 | fieldsSpec
890 |
891 | _getField: (name) ->
892 | def = {name: name, remote: name, type: null, model: null}
893 | if name of @constructor.fields
894 | return angular.extend(def, @constructor.fields[name] or {})
895 | def
896 |
897 | _toObject: ->
898 | obj = {}
899 |
900 | for own name, value of @
901 | if name in ['$id', '$meta', 'constructor', '__proto__']
902 | continue
903 | def = @_getField(name)
904 | obj[name] = value
905 | continue if not value
906 | if def.type is @constructor.Reference
907 | if angular.isObject(value) or (value instanceof Resource)
908 | obj[name] = if value.$id? then value.$id else null
909 | else if def.type is @constructor.ManyToMany
910 | values = if angular.isArray(value) then value else [ value ]
911 | result_values = []
912 | for value in values
913 | if angular.isObject(value) or (value instanceof Resource)
914 | result_values.push(if value.$id? then value.$id else null)
915 | else
916 | result_values.push(value)
917 | obj[name] = result_values
918 |
919 | obj
920 |
921 | _fromObject: (obj) ->
922 | data = angular.extend({}, @constructor.defaults, obj or {})
923 |
924 | for name, value of data
925 | if name in ['$id', '$meta', 'constructor', '__proto__']
926 | continue
927 | @[name] = value
928 |
929 | for name of @constructor.fields
930 | def = @_getField(name)
931 | if name in ['$id', '$meta', 'constructor', '__proto__']
932 | continue
933 | if not (name of data) and ('default' of def)
934 | @[name] = def.default
935 | @
936 |
937 | _toRemoteObject: ->
938 | obj = {}
939 |
940 | for own name, value of @
941 | if name in ['$id', '$meta', 'constructor', '__proto__']
942 | continue
943 | def = @_getField(name)
944 | obj[def.remote] = value
945 | continue if not value
946 | if def.type is @constructor.Reference
947 | if angular.isObject(value) or (value instanceof Resource)
948 | obj[def.remote] = if value.$id? then value.$id else null
949 | else if def.type is @constructor.ManyToMany
950 | values = if angular.isArray(value) then value else [ value ]
951 | result_values = []
952 | for value in values
953 | if angular.isObject(value) or (value instanceof Resource)
954 | result_values.push(if value.$id? then value.$id else null)
955 | else
956 | result_values.push(value)
957 | obj[def.remote] = result_values
958 |
959 | obj
960 |
961 | _fromRemoteObject: (obj) ->
962 | if isEmpty(@constructor.fields)
963 | data = angular.extend({}, @constructor.defaults, obj or {})
964 |
965 | for name, value of data
966 | if name in ['$id', '$meta', 'constructor', '__proto__']
967 | continue
968 | @[name] = value
969 | else
970 | data = angular.extend({}, obj or {})
971 |
972 | fieldsSpec = @_getFields()
973 |
974 | for name of fieldsSpec
975 | def = @_getField(name)
976 | if name in ['$id', '$meta', 'constructor', '__proto__']
977 | continue
978 | if def.remote of data
979 | @[name] = data[def.remote]
980 | else if 'default' of def
981 | @[name] = def.default
982 |
983 | for name, value of @constructor.defaults
984 | if not (name of @)
985 | @[name] = value
986 |
987 | if @constructor.idField of data
988 | @$id = obj[@constructor.idField]
989 | @
990 |
991 | Resource
992 | )
993 |
--------------------------------------------------------------------------------
/dist/angular-rest-orm.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Angular ORM for HTTP REST APIs
3 | * @version angular-rest-orm - v0.4.5 - 2014-11-11
4 | * @link https://github.com/panta/angular-rest-orm
5 | * @author Marco Pantaleoni
6 | *
7 | * Copyright (c) 2014 Marco Pantaleoni
8 | * Licensed under the MIT License, http://opensource.org/licenses/MIT
9 | */
10 | var __hasProp = {}.hasOwnProperty,
11 | __slice = [].slice,
12 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
13 |
14 | angular.module("restOrm", []).factory("Resource", ['$http', '$q', '$rootScope', function($http, $q, $rootScope) {
15 | var Resource, endsWith, isEmpty, isKeyLike, startsWith, urljoin, _urljoin;
16 | isEmpty = function(obj) {
17 | var key;
18 | if (obj == null) {
19 | return true;
20 | }
21 | if (obj.length > 0) {
22 | return false;
23 | }
24 | if (obj.length === 0) {
25 | return true;
26 | }
27 | for (key in obj) {
28 | if (!__hasProp.call(obj, key)) continue;
29 | return false;
30 | }
31 | return true;
32 | };
33 | startsWith = function(s, sub) {
34 | return s.slice(0, sub.length) === sub;
35 | };
36 | endsWith = function(s, sub) {
37 | return sub === '' || s.slice(-sub.length) === sub;
38 | };
39 | isKeyLike = function(value) {
40 | if (value == null) {
41 | return false;
42 | }
43 | if (angular.isUndefined(value) || (value === null)) {
44 | return false;
45 | }
46 | if (angular.isObject(value) || angular.isArray(value)) {
47 | return false;
48 | }
49 | return true;
50 | };
51 | _urljoin = function() {
52 | var c_url, component, components, i, last_comp, normalize, r, skip, url_result, _i, _j, _len, _ref;
53 | components = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
54 | normalize = function(str) {
55 | return str.replace(/[\/]+/g, "/").replace(/\/\?/g, "?").replace(/\/\#/g, "#").replace(/\:\//g, "://");
56 | };
57 | url_result = [];
58 | if (components && (components.length > 0)) {
59 | skip = 0;
60 | while (skip < components.length) {
61 | if (components[skip]) {
62 | break;
63 | }
64 | ++skip;
65 | }
66 | if (skip) {
67 | components = components.slice(skip);
68 | }
69 | }
70 | last_comp = null;
71 | for (_i = 0, _len = components.length; _i < _len; _i++) {
72 | component = components[_i];
73 | if (!((component != null) && component)) {
74 | continue;
75 | }
76 | last_comp = component;
77 | c_url = ("" + component).split("/");
78 | for (i = _j = 0, _ref = c_url.length; 0 <= _ref ? _j < _ref : _j > _ref; i = 0 <= _ref ? ++_j : --_j) {
79 | if (c_url[i] === "..") {
80 | url_result.pop();
81 | } else if ((c_url[i] === ".") || (c_url[i] === "")) {
82 | continue;
83 | } else {
84 | url_result.push(c_url[i]);
85 | }
86 | }
87 | }
88 | r = normalize(url_result.join("/"));
89 | if (components && (components.length >= 1)) {
90 | component = "" + components[0];
91 | if (startsWith(component, "//")) {
92 | r = "//" + r;
93 | } else if (startsWith(component, "/")) {
94 | r = "/" + r;
95 | }
96 | last_comp = "" + last_comp;
97 | if (endsWith(last_comp, "/") && (!endsWith(r, "/"))) {
98 | r = r + "/";
99 | }
100 | }
101 | return r;
102 | };
103 | urljoin = function() {
104 | return encodeURI(_urljoin.apply(null, arguments));
105 | };
106 |
107 | /**
108 | * @ngdoc service
109 | * @name restOrm.Resource
110 | *
111 | * @description
112 | * # Resource
113 | * Is the base class for RESTful resource models.
114 | *
115 | * Derive from `Resource` providing proper values for class properties
116 | * as described below to define models for your resources.
117 | *
118 | * A very minimal example in JavaScript:
119 | *
120 | * ```javascript
121 | * var Book = Resource.Subclass({}, {
122 | * urlEndpoint: '/api/books/',
123 | * idField: '_id'
124 | * });
125 | * ```
126 | *
127 | * of in CoffeeScript:
128 | *
129 | * ```coffeescript
130 | * class Book extends Resource
131 | * @.urlEndpoint: '/api/books/'
132 | * @.idField: '_id'
133 | * ```
134 | * (the `.` has been added to circumvent an `ngdoc` bug regarding parsing of `@` in blockquotes...)
135 | *
136 | * @returns {object} Resource class
137 | */
138 | Resource = (function() {
139 |
140 | /**
141 | * @ngdoc property {String}
142 | * @name .#urlPrefix
143 | * @propertyOf restOrm.Resource
144 | *
145 | * @description
146 | * # urlPrefix
147 | * **class property** - prefix that will be prepended to all URLs
148 | * for this resource.
149 | * Defaults to the empty string (in this case, nothing will be prepended).
150 | *
151 | * The final base URL will have the form
152 | *
153 | * `urlPrefix` / `urlEndpoint`
154 | *
155 | * (Note that slashes will be added only where necessary)
156 | *
157 | * This property is intended to be specified on subclasses of `Resource`.
158 | */
159 | Resource.urlPrefix = '';
160 |
161 |
162 | /**
163 | * @ngdoc property {String}
164 | * @name .#urlEndpoint
165 | * @propertyOf restOrm.Resource
166 | *
167 | * @description
168 | * # urlEndpoint
169 | * **class property** - the base URL for the resource.
170 | *
171 | * The final base URL will have the form
172 | *
173 | * `urlPrefix` / `urlEndpoint`
174 | *
175 | * (Note that slashes will be added only where necessary)
176 | *
177 | * This property is intended to be specified on subclasses of `Resource`.
178 | */
179 |
180 | Resource.urlEndpoint = '';
181 |
182 |
183 | /**
184 | * @ngdoc property {String}
185 | * @name .#idField
186 | * @propertyOf restOrm.Resource
187 | *
188 | * @description
189 | * # idField
190 | * **class property** - (optional) the name of the field containing the ID of the resource in
191 | * remote endpoint responses.
192 | *
193 | * Defaults to `id`.
194 | *
195 | * This property is intended to be specified on subclasses of `Resource`.
196 | */
197 |
198 | Resource.idField = 'id';
199 |
200 |
201 | /**
202 | * @ngdoc property {Object}
203 | * @name .#fields
204 | * @propertyOf restOrm.Resource
205 | *
206 | * @description
207 | * # fields
208 | * **class property** - (optional) object specifying names and kinds of resource fields.
209 | *
210 | * It's possible to specify an entry for each field, with this form:
211 | *
212 | * ```javascript
213 | * {
214 | * ...
215 | * NAME: {
216 | * default: DEFAULT_VALUE,
217 | * remote: REMOTE_FIELD_NAME,
218 | * type: FIELD_TYPE,
219 | * model: RELATED_MODEL
220 | * },
221 | * ...
222 | * }
223 | * ```
224 | *
225 | * where:
226 | *
227 | * - *NAME* is the field name as used on the resource (model instance). This can be
228 | * different from the remote endpoint field name.
229 | * - *DEFAULT_VALUE* is the default value for the field, used if the remote doesn't
230 | * provide a value or when creating a resource without specifying all fields
231 | * - *REMOTE_FIELD_NAME* is the (optional) field name on the remote endpoint. If not
232 | * specified, it's assumed to be the same as *NAME*
233 | * - *FIELD_TYPE* at this time is used only to specify relations. If specified can
234 | * be `Resource.Reference` or `Resource.ManyToMany`
235 | * - *RELATED_MODEL* used only for relations, specifies the related model (must be a
236 | * `Resource` subclass)
237 | *
238 | * All of the entry object fields are optional.
239 | *
240 | *
241 | * If `fields` is not specified, fields will be fetched and copied between
242 | * responses and resource models as-is.
243 | *
244 | * This property is intended to be specified on subclasses of `Resource`.
245 | */
246 |
247 | Resource.fields = {};
248 |
249 |
250 | /**
251 | * @ngdoc property {Object}
252 | * @name .#defaults
253 | * @propertyOf restOrm.Resource
254 | *
255 | * @description
256 | * # defaults
257 | * **class property** - (optional) object specifying default values for resource fields.
258 | * It is meant to be an easy shortcut for those cases where the `fields` complexity is
259 | * not needed.
260 | *
261 | * This property is intended to be specified on subclasses of `Resource`.
262 | */
263 |
264 | Resource.defaults = {};
265 |
266 | Resource.headers = {};
267 |
268 | Resource.prepareRequest = null;
269 |
270 | Resource.transformResponse = null;
271 |
272 | Resource.postResponse = null;
273 |
274 | Resource.Reference = 'reference';
275 |
276 | Resource.ManyToMany = 'many2many';
277 |
278 | Resource.include = function(obj) {
279 | var key, value, _ref;
280 | if (!obj) {
281 | throw new Error('include(obj) requires obj');
282 | }
283 | for (key in obj) {
284 | value = obj[key];
285 | if (key !== 'included' && key !== 'extended') {
286 | this.prototype[key] = value;
287 | }
288 | }
289 | if ((_ref = obj.included) != null) {
290 | _ref.apply(this);
291 | }
292 | return this;
293 | };
294 |
295 | Resource.extend = function(obj) {
296 | var key, value, _ref;
297 | if (!obj) {
298 | throw new Error('extend(obj) requires obj');
299 | }
300 | for (key in obj) {
301 | value = obj[key];
302 | if (key !== 'included' && key !== 'extended') {
303 | this[key] = value;
304 | }
305 | }
306 | if ((_ref = obj.extended) != null) {
307 | _ref.apply(this);
308 | }
309 | return this;
310 | };
311 |
312 |
313 | /**
314 | * @ngdoc method
315 | * @name Resource#Subclass
316 | * @methodOf restOrm.Resource
317 | *
318 | * @description
319 | * # Resource.Subclass()
320 | * **class method** that returns a new subclass derived from `Resource`
321 | * extended with the specified instance and class properties.
322 | *
323 | * This method is intended to be used from plain *JavaScript*.
324 | *
325 | * *CoffeeScript* users should rely on the native `class ... extends ...` syntax
326 | * to create `Resource` subclasses.
327 | *
328 | * @param {object} instances Properties to add to instances of the newly created class
329 | *
330 | * @param {object} statics Class properties of the newly created class
331 | *
332 | * @returns {function} the new class (a constructor function)
333 | */
334 |
335 | Resource.Subclass = function(instances, statics) {
336 | var Result;
337 | Result = (function(_super) {
338 | __extends(Result, _super);
339 |
340 | function Result() {
341 | return Result.__super__.constructor.apply(this, arguments);
342 | }
343 |
344 | return Result;
345 |
346 | })(this);
347 | if (instances) {
348 | Result.include(instances);
349 | }
350 | if (statics) {
351 | Result.extend(statics);
352 | }
353 | Result.prototype.$super = function(method) {
354 | return this.constructor.__super__[method];
355 | };
356 | return Result;
357 | };
358 |
359 |
360 | /**
361 | * @ngdoc method
362 | * @name Resource
363 | * @methodOf restOrm.Resource
364 | *
365 | * @description
366 | * # Resource()
367 | * **constructor** for `Resource`
368 | *
369 | * Usually one would never call this constructor direcly, but always through subclasses.
370 | *
371 | * @param {object|null=} data Object that will be used to initialize the resource (model instance)
372 | *
373 | * @param {object=} opts Options
374 | */
375 |
376 | function Resource(data, opts) {
377 | if (data == null) {
378 | data = null;
379 | }
380 | if (opts == null) {
381 | opts = {};
382 | }
383 | this.$meta = {
384 | persisted: false,
385 | async: {
386 | direct: {
387 | deferred: null,
388 | resolved: true
389 | },
390 | m2o: {
391 | deferred: null,
392 | resolved: true
393 | },
394 | m2m: {
395 | deferred: null,
396 | resolved: true
397 | }
398 | }
399 | };
400 | angular.extend(this.$meta, opts);
401 | this.$error = null;
402 | this.$id = null;
403 | this._fromObject(data || {});
404 | this.$promise = null;
405 | this.$promiseDirect = null;
406 | this._setupPromises();
407 | this._fetchRelations();
408 | if (typeof this.$initialize === "function") {
409 | this.$initialize.apply(this, arguments);
410 | }
411 | }
412 |
413 |
414 | /**
415 | * @ngdoc method
416 | * @name Resource#Create
417 | * @methodOf restOrm.Resource
418 | *
419 | * @description
420 | * # Resource.Create()
421 | * **class method ** - creates a model resource (instance) and persists it on the remote side.
422 | *
423 | * Usually this method will be called on a `Resource` subclass.
424 | *
425 | * @param {data|null=} data Object used to initialize the new model instance properties
426 | *
427 | * @param {object=} opts Options passed to `$save()`
428 | *
429 | * @returns {object} newly created resource (model instance)
430 | */
431 |
432 | Resource.Create = function(data, opts) {
433 | var item;
434 | if (data == null) {
435 | data = null;
436 | }
437 | if (opts == null) {
438 | opts = {};
439 | }
440 | data = data || this.defaults;
441 | item = new this(data, {
442 | persisted: false
443 | });
444 | item.$save(opts);
445 | return item;
446 | };
447 |
448 |
449 | /**
450 | * @ngdoc method
451 | * @name Resource#Get
452 | * @methodOf restOrm.Resource
453 | *
454 | * @description
455 | * # Resource.Get
456 | * **class method ** - fetches and returns a resource with the given id.
457 | *
458 | * A model instance for the resource is constructed and returned, and it
459 | * will be populated with the contents fetched from the remote endpoint
460 | * for the resource with the specified `id`.
461 | *
462 | * The HTTP fetch will be performed asynchronously, so the model instance,
463 | * even if returned immediately, will be populated in an
464 | * incremental fashion.
465 | *
466 | * To allow the user to be notified on the completion of the fetch process,
467 | * the model instance contains as special properties a couple of promises that
468 | * will be fulfilled when the fetch completes.
469 | *
470 | * The first one is named `$promise`. This will be fulfilled when
471 | * the resource will have been fetched **along with all its relations**
472 | * (and the relations of the relations... down to the deepest nesting levels).
473 | *
474 | * The other one is named `$promiseDirect`. This will be fulfilled when the
475 | * resource will have been fetched, but ignoring relations.
476 | *
477 | * The method will cause an http request of the form:
478 | *
479 | * `GET` *RESOURCE_URL* / *id*
480 | *
481 | * Usually this method will be called on a `Resource` subclass.
482 | *
483 | * @param {Number|String} id Remote endpoint id of the resource to fetch
484 | *
485 | * @param {object=} opts Options object containing optional `params` and `data`
486 | * fields passed to the respective counterparts of the `$http` call
487 | *
488 | * @returns {object} fetched resource (model instance)
489 | */
490 |
491 | Resource.Get = function(id, opts) {
492 | var item, req, url;
493 | if (opts == null) {
494 | opts = {};
495 | }
496 | item = new this();
497 | url = urljoin(this._GetURLBase(), id);
498 | item._setupPromises();
499 | req = this._PrepareRequest({
500 | id: id,
501 | opts: opts,
502 | what: 'Get',
503 | method: 'GET',
504 | url: url,
505 | headers: this._BuildHeaders('Get', 'GET', null),
506 | params: opts.params || {},
507 | data: opts.data || {},
508 | item: item
509 | });
510 | $http({
511 | method: req.method,
512 | url: req.url,
513 | headers: req.headers,
514 | params: req.params,
515 | data: req.data
516 | }).then(((function(_this) {
517 | return function(response) {
518 | var res;
519 | res = _this._TransformResponse(req, response);
520 | item._fromRemote(res.data);
521 | return _this._PostResponse(res);
522 | };
523 | })(this)), function(response) {
524 | item.$error = response;
525 | item.resolvePromise(item.$meta.async.direct.deferred, false);
526 | item.resolvePromise(item.$meta.async.m2o.deferred, false);
527 | return item.resolvePromise(item.$meta.async.m2m.deferred, false);
528 | });
529 | return item;
530 | };
531 |
532 |
533 | /**
534 | * @ngdoc method
535 | * @name Resource#All
536 | * @methodOf restOrm.Resource
537 | *
538 | * @description
539 | * # Resource.All
540 | * **class method ** - fetches and returns all the remote resources.
541 | *
542 | * This method returns a *collection*, an Array augmented with some
543 | * additional properties, as detailed below.
544 | *
545 | * The HTTP fetch will be performed asynchronously, so the collection,
546 | * even if returned immediately, will be filled with results in an
547 | * incremental fashion.
548 | *
549 | * To allow the user to be notified on the completion of the fetch process,
550 | * the collection contains as special properties a couple of promises that
551 | * will be fulfilled when the fetch completes.
552 | *
553 | * The first one is named `$promise`. This will be fulfilled when all
554 | * collection items will have been fetched **along with all their relations**
555 | * (and the relations of the relations... down to the deepest nesting levels).
556 | *
557 | * The other one is named `$promiseDirect`. This will be fulfilled when all
558 | * collection items will have been fetched, but ignoring relations.
559 | *
560 | * The method will cause an http request of the form:
561 | *
562 | * `GET` *RESOURCE_URL* /
563 | *
564 | * Usually this method will be called on a `Resource` subclass.
565 | *
566 | * @param {object=} opts Options object containing optional `params` and `data`
567 | * fields passed to the respective counterparts of the `$http` call
568 | *
569 | * @returns {object} fetched collection (augmented array of model instances)
570 | */
571 |
572 | Resource.All = function(opts) {
573 | var collection, req, url;
574 | if (opts == null) {
575 | opts = {};
576 | }
577 | collection = this._MakeCollection();
578 | url = urljoin(this._GetURLBase());
579 | req = this._PrepareRequest({
580 | opts: opts,
581 | what: 'All',
582 | method: 'GET',
583 | url: url,
584 | headers: this._BuildHeaders('All', 'GET', null),
585 | params: opts.params || {},
586 | data: opts.data || {},
587 | collection: collection
588 | });
589 | $http({
590 | method: req.method,
591 | url: req.url,
592 | headers: req.headers,
593 | params: req.params,
594 | data: req.data
595 | }).then(((function(_this) {
596 | return function(response) {
597 | var res, values, _i, _len, _ref;
598 | res = _this._TransformResponse(req, response);
599 | _ref = res.data;
600 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
601 | values = _ref[_i];
602 | collection.push(_this._MakeInstanceFromRemote(values));
603 | }
604 | _this._PostResponse(res);
605 | return collection.$finalize();
606 | };
607 | })(this)), (function(_this) {
608 | return function(response) {
609 | var res;
610 | res = _this._TransformResponse(req, response);
611 | _this._PostResponse(res);
612 | return collection.$finalize(false, response);
613 | };
614 | })(this));
615 | return collection;
616 | };
617 |
618 | Resource.Search = function(field, value, opts) {
619 | var collection, req, url;
620 | if (opts == null) {
621 | opts = {};
622 | }
623 | collection = this._MakeCollection();
624 | url = urljoin(this._GetURLBase(), "search", field, value);
625 | req = this._PrepareRequest({
626 | field: field,
627 | value: value,
628 | opts: opts,
629 | what: 'Search',
630 | method: 'GET',
631 | url: url,
632 | headers: this._BuildHeaders('Search', 'GET', null),
633 | params: opts.params || {},
634 | data: opts.data || {},
635 | collection: collection
636 | });
637 | $http({
638 | method: req.method,
639 | url: req.url,
640 | headers: req.headers,
641 | params: req.params,
642 | data: req.data
643 | }).then(((function(_this) {
644 | return function(response) {
645 | var res, values, _i, _len, _ref;
646 | res = _this._TransformResponse(req, response);
647 | _ref = res.data;
648 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
649 | values = _ref[_i];
650 | collection.push(_this._MakeInstanceFromRemote(values));
651 | }
652 | _this._PostResponse(res);
653 | return collection.$finalize();
654 | };
655 | })(this)), (function(_this) {
656 | return function(response) {
657 | var res;
658 | res = _this._TransformResponse(req, response);
659 | _this._PostResponse(res);
660 | return collection.$finalize(false, response);
661 | };
662 | })(this));
663 | return collection;
664 | };
665 |
666 |
667 | /**
668 | * @ngdoc method
669 | * @name Resource#$save
670 | * @methodOf restOrm.Resource
671 | *
672 | * @description
673 | * Saves the resource represented by this model instance.
674 | *
675 | * If the model instance represents a resource obtained from the
676 | * remote endpoint (thus having an ID), than the method will update
677 | * the remote resource (using an HTTP `PUT`), otherwise it will create
678 | * the resource on the remote endpoint (using an HTTP `POST`).
679 | *
680 | * The operation will update the model representation with the data
681 | * obtained from the remote endpoint.
682 | *
683 | * The HTTP operation will be performed asynchronously, so the `$promise`
684 | * and `$promiseDirect` promises will be re-generated to inform of the
685 | * completion.
686 | *
687 | * For a new resource, the method will cause an http request of the form:
688 | *
689 | * `POST` *RESOURCE_URL* /
690 | *
691 | * otherwise for an existing resource:
692 | *
693 | * `PUT` *RESOURCE_URL* / *id*
694 | *
695 | * Usually this method will be called on a `Resource` subclass.
696 | *
697 | * @param {object=} opts Options object containing optional `params` and `data`
698 | * fields passed to the respective counterparts of the `$http` call
699 | *
700 | * @returns {object} the model instance itself
701 | */
702 |
703 | Resource.prototype.$save = function(opts) {
704 | var data, method, req, url;
705 | if (opts == null) {
706 | opts = {};
707 | }
708 | data = this._toRemoteObject();
709 | if (this.$meta.persisted && (this.$id != null)) {
710 | method = 'PUT';
711 | url = urljoin(this._getURLBase(), this.$id);
712 | } else {
713 | method = 'POST';
714 | if (this.constructor.idField in data) {
715 | delete data[this.constructor.idField];
716 | }
717 | url = urljoin(this._getURLBase());
718 | }
719 | this._setupPromises();
720 | req = this._prepareRequest({
721 | opts: opts,
722 | what: '$save',
723 | method: method,
724 | url: url,
725 | headers: this._buildHeaders('$save', method),
726 | params: opts.params || {},
727 | data: data,
728 | cache: false,
729 | item: this
730 | });
731 | $http({
732 | method: req.method,
733 | url: req.url,
734 | data: req.data,
735 | cache: req.cache,
736 | headers: req.headers,
737 | params: req.params
738 | }).then(((function(_this) {
739 | return function(response) {
740 | var res;
741 | res = _this._transformResponse(req, response);
742 | _this._fromRemote(res.data);
743 | return _this._postResponse(res);
744 | };
745 | })(this)), (function(_this) {
746 | return function(response) {
747 | _this.$error = response;
748 | _this.resolvePromise(_this.$meta.async.direct.deferred, false);
749 | _this.resolvePromise(_this.$meta.async.m2o.deferred, false);
750 | return _this.resolvePromise(_this.$meta.async.m2m.deferred, false);
751 | };
752 | })(this));
753 | return this;
754 | };
755 |
756 | Resource._MakeCollection = function() {
757 | var collection;
758 | collection = [];
759 | collection.$useApplyAsync = false;
760 | collection.$meta = {
761 | model: this,
762 | async: {
763 | direct: {
764 | deferred: $q.defer(),
765 | resolved: false
766 | },
767 | complete: {
768 | deferred: $q.defer(),
769 | resolved: false
770 | }
771 | }
772 | };
773 | collection.$error = false;
774 | collection.$promise = collection.$meta.async.complete.deferred.promise;
775 | collection.$promiseDirect = collection.$meta.async.direct.deferred.promise;
776 | collection.$getItemsPromises = function() {
777 | var instance, _i, _len, _results;
778 | _results = [];
779 | for (_i = 0, _len = collection.length; _i < _len; _i++) {
780 | instance = collection[_i];
781 | _results.push(instance.$promise);
782 | }
783 | return _results;
784 | };
785 | collection.$getItemsPromiseDirects = function() {
786 | var instance, _i, _len, _results;
787 | _results = [];
788 | for (_i = 0, _len = collection.length; _i < _len; _i++) {
789 | instance = collection[_i];
790 | _results.push(instance.$promiseDirect);
791 | }
792 | return _results;
793 | };
794 | collection.$_getPromiseForItems = function() {
795 | return $q.all(collection.$getItemsPromises());
796 | };
797 | collection.$_getPromiseDirectForItems = function() {
798 | return $q.all(collection.$getItemsPromiseDirects());
799 | };
800 | collection._resolvePromise = function(deferred, success) {
801 | if (success == null) {
802 | success = true;
803 | }
804 | if (success) {
805 | return deferred.resolve(collection);
806 | }
807 | return deferred.reject(collection);
808 | };
809 | collection.resolvePromise = function(deferred, success) {
810 | if (success == null) {
811 | success = true;
812 | }
813 | if (collection.$useApplyAsync) {
814 | $rootScope.$applyAsync(function() {
815 | return collection._resolvePromise(deferred, success);
816 | });
817 | } else {
818 | collection._resolvePromise(deferred, success);
819 | if (!$rootScope.$$phase) {
820 | $rootScope.$apply();
821 | }
822 | }
823 | return collection;
824 | };
825 | collection.$finalize = function(success, response) {
826 | var items_success;
827 | if (success == null) {
828 | success = true;
829 | }
830 | if (response == null) {
831 | response = null;
832 | }
833 | items_success = success;
834 | collection.$_getPromiseForItems().then((function() {
835 | collection.resolvePromise(collection.$meta.async.complete.deferred, success);
836 | return collection.$meta.async.complete.resolved = true;
837 | }), function() {
838 | items_success = false;
839 | collection.resolvePromise(collection.$meta.async.complete.deferred, items_success);
840 | collection.$meta.async.complete.resolved = true;
841 | return collection.$error = true;
842 | });
843 | collection.resolvePromise(collection.$meta.async.direct.deferred, success);
844 | collection.$meta.async.direct.resolved = true;
845 | if ((!success) && response) {
846 | collection.$error = response;
847 | } else {
848 | collection.$error = !(success && items_success);
849 | }
850 | return collection;
851 | };
852 | return collection;
853 | };
854 |
855 | Resource._MakeInstanceFromRemote = function(data) {
856 | var instance;
857 | instance = new this();
858 | instance._setupPromises();
859 | instance._fromRemote(data);
860 | return instance;
861 | };
862 |
863 | Resource._GetURLBase = function() {
864 | return _urljoin(this.urlPrefix, this.urlEndpoint);
865 | };
866 |
867 | Resource._BuildHeaders = function(what, method, instance) {
868 | var dst_headers, processHeaderSource;
869 | if (what == null) {
870 | what = null;
871 | }
872 | if (method == null) {
873 | method = null;
874 | }
875 | if (instance == null) {
876 | instance = null;
877 | }
878 | if (this.headers == null) {
879 | return {};
880 | }
881 | if (angular.isFunction(this.headers)) {
882 | return this.headers.call(this, {
883 | klass: this,
884 | what: what,
885 | method: method,
886 | instance: instance
887 | });
888 | } else if (angular.isObject(this.headers)) {
889 | processHeaderSource = (function(_this) {
890 | return function(dst, src) {
891 | var dst_value, name, value;
892 | for (name in src) {
893 | value = src[name];
894 | if (angular.isFunction(value)) {
895 | dst_value = value.call(_this, {
896 | klass: _this,
897 | what: what,
898 | method: method,
899 | instance: instance
900 | });
901 | } else {
902 | dst_value = value;
903 | }
904 | if (dst_value !== null) {
905 | dst[name] = dst_value;
906 | }
907 | }
908 | return dst;
909 | };
910 | })(this);
911 | dst_headers = {};
912 | if (('common' in this.headers) || ((what != null) && (what in this.headers)) || ((method != null) && (method in this.headers))) {
913 | if ('common' in this.headers) {
914 | processHeaderSource(dst_headers, this.headers.common);
915 | }
916 | if ((what != null) && (what in this.headers)) {
917 | processHeaderSource(dst_headers, this.headers[what]);
918 | }
919 | if ((method != null) && (method in this.headers)) {
920 | processHeaderSource(dst_headers, this.headers[method]);
921 | }
922 | } else {
923 | processHeaderSource(dst_headers, this.headers);
924 | }
925 | return dst_headers;
926 | }
927 | return {};
928 | };
929 |
930 | Resource._PrepareRequest = function(req) {
931 | req.klass = this;
932 | if ((this.prepareRequest != null) && angular.isFunction(this.prepareRequest)) {
933 | return this.prepareRequest.call(this, req);
934 | }
935 | return req;
936 | };
937 |
938 | Resource.prototype._prepareRequest = function(req) {
939 | req.instance = this;
940 | return this.constructor._PrepareRequest(req);
941 | };
942 |
943 | Resource._TransformResponse = function(req, http_response, instance) {
944 | var res;
945 | if (instance == null) {
946 | instance = null;
947 | }
948 | res = angular.extend({}, req, {
949 | request: req,
950 | response: http_response,
951 | data: http_response.data
952 | });
953 | res.klass = this;
954 | res.instance = instance;
955 | res.data = res.response.data;
956 | res.status = res.response.status;
957 | res.headers = res.response.headers;
958 | res.config = res.response.config;
959 | res.statusText = res.response.statusText;
960 | if ((this.transformResponse != null) && angular.isFunction(this.transformResponse)) {
961 | return this.transformResponse.call(this, res);
962 | }
963 | return res;
964 | };
965 |
966 | Resource.prototype._transformResponse = function(req, http_response) {
967 | return this.constructor._TransformResponse(req, http_response, this);
968 | };
969 |
970 | Resource._PostResponse = function(res) {
971 | res.klass = this;
972 | if ((this.postResponse != null) && angular.isFunction(this.postResponse)) {
973 | return this.postResponse.call(this, res);
974 | }
975 | return res;
976 | };
977 |
978 | Resource.prototype._postResponse = function(res) {
979 | res.instance = this;
980 | return this.constructor._PostResponse(res);
981 | };
982 |
983 | Resource.prototype._buildHeaders = function(what, method) {
984 | if (what == null) {
985 | what = null;
986 | }
987 | if (method == null) {
988 | method = null;
989 | }
990 | return this.constructor._BuildHeaders(what, method, this);
991 | };
992 |
993 | Resource.prototype._setupPromises = function() {
994 | var changed;
995 | changed = false;
996 | if (this.$meta.async.direct.resolved || (this.$meta.async.direct.deferred == null)) {
997 | this.$meta.async.direct.deferred = $q.defer();
998 | this.$meta.async.direct.resolved = false;
999 | changed = true;
1000 | this.$promiseDirect = this.$meta.async.direct.deferred.promise.then((function(_this) {
1001 | return function() {
1002 | _this.$meta.async.direct.resolved = true;
1003 | return _this;
1004 | };
1005 | })(this));
1006 | }
1007 | if (this.$meta.async.m2o.resolved || (this.$meta.async.m2o.deferred == null)) {
1008 | this.$meta.async.m2o.deferred = $q.defer();
1009 | this.$meta.async.m2o.resolved = false;
1010 | changed = true;
1011 | this.$meta.async.m2o.deferred.promise.then((function(_this) {
1012 | return function() {
1013 | _this.$meta.async.m2o.resolved = true;
1014 | return _this;
1015 | };
1016 | })(this));
1017 | }
1018 | if (this.$meta.async.m2m.resolved || (this.$meta.async.m2m.deferred == null)) {
1019 | this.$meta.async.m2m.deferred = $q.defer();
1020 | this.$meta.async.m2m.resolved = false;
1021 | changed = true;
1022 | this.$meta.async.m2m.deferred.promise.then((function(_this) {
1023 | return function() {
1024 | _this.$meta.async.m2m.resolved = true;
1025 | return _this;
1026 | };
1027 | })(this));
1028 | }
1029 | if (changed) {
1030 | this.$promise = $q.all([this.$meta.async.direct.deferred.promise, this.$meta.async.m2o.deferred.promise, this.$meta.async.m2m.deferred.promise]).then((function(_this) {
1031 | return function() {
1032 | _this.$meta.async.direct.resolved = true;
1033 | _this.$meta.async.m2o.resolved = true;
1034 | _this.$meta.async.m2m.resolved = true;
1035 | return _this;
1036 | };
1037 | })(this));
1038 | }
1039 | return this;
1040 | };
1041 |
1042 | Resource.prototype._getURLBase = function() {
1043 | return _urljoin(this.constructor.urlPrefix, this.constructor.urlEndpoint);
1044 | };
1045 |
1046 | Resource.prototype._fetchRelations = function() {
1047 | if (this.$id != null) {
1048 | this._fetchReferences();
1049 | this._fetchM2M();
1050 | }
1051 | return this;
1052 | };
1053 |
1054 | Resource.prototype._fetchReferences = function() {
1055 | var def, fetchReference, name, promises;
1056 | fetchReference = function(instance, reference, promises) {
1057 | var fieldName, record, ref_id;
1058 | fieldName = reference.name;
1059 | if ((fieldName in instance) && (instance[fieldName] != null) && isKeyLike(instance[fieldName])) {
1060 | ref_id = instance[fieldName];
1061 | record = reference.model.Get(ref_id);
1062 | instance[fieldName] = record;
1063 | return promises.push(record.$promise);
1064 | }
1065 | };
1066 | promises = [];
1067 | for (name in this.constructor.fields) {
1068 | def = this._getField(name);
1069 | if (def.type === this.constructor.Reference) {
1070 | fetchReference(this, def, promises);
1071 | }
1072 | }
1073 | $q.all(promises).then(((function(_this) {
1074 | return function() {
1075 | return _this.resolvePromise(_this.$meta.async.m2o.deferred);
1076 | };
1077 | })(this)), (function(_this) {
1078 | return function() {
1079 | return _this.resolvePromise(_this.$meta.async.m2o.deferred, false);
1080 | };
1081 | })(this));
1082 | return this;
1083 | };
1084 |
1085 | Resource.prototype._fetchM2M = function() {
1086 | var collections, def, fetchM2M, name, promises, refs_collection, _i, _len;
1087 | fetchM2M = function(instance, m2m, promises, collections) {
1088 | var fieldName, record, ref_id, refs_collection, refs_promises, _i, _len, _ref;
1089 | fieldName = m2m.name;
1090 | if ((fieldName in instance) && (instance[fieldName] != null) && angular.isArray(instance[fieldName])) {
1091 | refs_promises = [];
1092 | refs_collection = m2m.model._MakeCollection();
1093 | _ref = instance[fieldName];
1094 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
1095 | ref_id = _ref[_i];
1096 | record = m2m.model.Get(ref_id);
1097 | refs_collection.push(record);
1098 | refs_promises.push(record.$promise);
1099 | }
1100 | instance[fieldName] = refs_collection;
1101 | promises.push(refs_collection.$promise);
1102 | return collections.push(refs_collection);
1103 | } else {
1104 | return instance[fieldName] = [];
1105 | }
1106 | };
1107 | promises = [];
1108 | collections = [];
1109 | for (name in this.constructor.fields) {
1110 | def = this._getField(name);
1111 | if (def.type === this.constructor.ManyToMany) {
1112 | fetchM2M(this, def, promises, collections);
1113 | }
1114 | }
1115 | $q.all(promises).then(((function(_this) {
1116 | return function() {
1117 | return _this.resolvePromise(_this.$meta.async.m2m.deferred);
1118 | };
1119 | })(this)), (function(_this) {
1120 | return function() {
1121 | return _this.resolvePromise(_this.$meta.async.m2m.deferred, false);
1122 | };
1123 | })(this));
1124 | for (_i = 0, _len = collections.length; _i < _len; _i++) {
1125 | refs_collection = collections[_i];
1126 | refs_collection.$finalize();
1127 | }
1128 | return this;
1129 | };
1130 |
1131 | Resource.prototype._fromRemote = function(data) {
1132 | this._fromRemoteObject(data);
1133 | this.$meta.persisted = true;
1134 | this.resolvePromise(this.$meta.async.direct.deferred);
1135 | this._fetchRelations();
1136 | return this;
1137 | };
1138 |
1139 | Resource.prototype._resolvePromise = function(deferred, success) {
1140 | if (success == null) {
1141 | success = true;
1142 | }
1143 | if (success) {
1144 | return deferred.resolve(this);
1145 | }
1146 | return deferred.reject(this);
1147 | };
1148 |
1149 | Resource.prototype.resolvePromise = function(deferred, success) {
1150 | if (success == null) {
1151 | success = true;
1152 | }
1153 | if (this.$useApplyAsync) {
1154 | $rootScope.$applyAsync((function(_this) {
1155 | return function() {
1156 | return _this._resolvePromise(deferred, success);
1157 | };
1158 | })(this));
1159 | } else {
1160 | this._resolvePromise(deferred, success);
1161 | if (!$rootScope.$$phase) {
1162 | $rootScope.$apply();
1163 | }
1164 | }
1165 | return this;
1166 | };
1167 |
1168 | Resource.prototype._getFields = function() {
1169 | var fieldsSpec, name, value, _ref;
1170 | fieldsSpec = {};
1171 | _ref = this.constructor.defaults;
1172 | for (name in _ref) {
1173 | value = _ref[name];
1174 | fieldsSpec[name] = {
1175 | "default": value
1176 | };
1177 | }
1178 | if (!(this.constructor.idField in fieldsSpec)) {
1179 | fieldsSpec[this.constructor.idField] = {
1180 | "default": null
1181 | };
1182 | }
1183 | angular.extend(fieldsSpec, this.constructor.fields);
1184 | return fieldsSpec;
1185 | };
1186 |
1187 | Resource.prototype._getField = function(name) {
1188 | var def;
1189 | def = {
1190 | name: name,
1191 | remote: name,
1192 | type: null,
1193 | model: null
1194 | };
1195 | if (name in this.constructor.fields) {
1196 | return angular.extend(def, this.constructor.fields[name] || {});
1197 | }
1198 | return def;
1199 | };
1200 |
1201 | Resource.prototype._toObject = function() {
1202 | var def, name, obj, result_values, value, values, _i, _len;
1203 | obj = {};
1204 | for (name in this) {
1205 | if (!__hasProp.call(this, name)) continue;
1206 | value = this[name];
1207 | if (name === '$id' || name === '$meta' || name === 'constructor' || name === '__proto__') {
1208 | continue;
1209 | }
1210 | def = this._getField(name);
1211 | obj[name] = value;
1212 | if (!value) {
1213 | continue;
1214 | }
1215 | if (def.type === this.constructor.Reference) {
1216 | if (angular.isObject(value) || (value instanceof Resource)) {
1217 | obj[name] = value.$id != null ? value.$id : null;
1218 | }
1219 | } else if (def.type === this.constructor.ManyToMany) {
1220 | values = angular.isArray(value) ? value : [value];
1221 | result_values = [];
1222 | for (_i = 0, _len = values.length; _i < _len; _i++) {
1223 | value = values[_i];
1224 | if (angular.isObject(value) || (value instanceof Resource)) {
1225 | result_values.push(value.$id != null ? value.$id : null);
1226 | } else {
1227 | result_values.push(value);
1228 | }
1229 | }
1230 | obj[name] = result_values;
1231 | }
1232 | }
1233 | return obj;
1234 | };
1235 |
1236 | Resource.prototype._fromObject = function(obj) {
1237 | var data, def, name, value;
1238 | data = angular.extend({}, this.constructor.defaults, obj || {});
1239 | for (name in data) {
1240 | value = data[name];
1241 | if (name === '$id' || name === '$meta' || name === 'constructor' || name === '__proto__') {
1242 | continue;
1243 | }
1244 | this[name] = value;
1245 | }
1246 | for (name in this.constructor.fields) {
1247 | def = this._getField(name);
1248 | if (name === '$id' || name === '$meta' || name === 'constructor' || name === '__proto__') {
1249 | continue;
1250 | }
1251 | if (!(name in data) && ('default' in def)) {
1252 | this[name] = def["default"];
1253 | }
1254 | }
1255 | return this;
1256 | };
1257 |
1258 | Resource.prototype._toRemoteObject = function() {
1259 | var def, name, obj, result_values, value, values, _i, _len;
1260 | obj = {};
1261 | for (name in this) {
1262 | if (!__hasProp.call(this, name)) continue;
1263 | value = this[name];
1264 | if (name === '$id' || name === '$meta' || name === 'constructor' || name === '__proto__') {
1265 | continue;
1266 | }
1267 | def = this._getField(name);
1268 | obj[def.remote] = value;
1269 | if (!value) {
1270 | continue;
1271 | }
1272 | if (def.type === this.constructor.Reference) {
1273 | if (angular.isObject(value) || (value instanceof Resource)) {
1274 | obj[def.remote] = value.$id != null ? value.$id : null;
1275 | }
1276 | } else if (def.type === this.constructor.ManyToMany) {
1277 | values = angular.isArray(value) ? value : [value];
1278 | result_values = [];
1279 | for (_i = 0, _len = values.length; _i < _len; _i++) {
1280 | value = values[_i];
1281 | if (angular.isObject(value) || (value instanceof Resource)) {
1282 | result_values.push(value.$id != null ? value.$id : null);
1283 | } else {
1284 | result_values.push(value);
1285 | }
1286 | }
1287 | obj[def.remote] = result_values;
1288 | }
1289 | }
1290 | return obj;
1291 | };
1292 |
1293 | Resource.prototype._fromRemoteObject = function(obj) {
1294 | var data, def, fieldsSpec, name, value, _ref;
1295 | if (isEmpty(this.constructor.fields)) {
1296 | data = angular.extend({}, this.constructor.defaults, obj || {});
1297 | for (name in data) {
1298 | value = data[name];
1299 | if (name === '$id' || name === '$meta' || name === 'constructor' || name === '__proto__') {
1300 | continue;
1301 | }
1302 | this[name] = value;
1303 | }
1304 | } else {
1305 | data = angular.extend({}, obj || {});
1306 | fieldsSpec = this._getFields();
1307 | for (name in fieldsSpec) {
1308 | def = this._getField(name);
1309 | if (name === '$id' || name === '$meta' || name === 'constructor' || name === '__proto__') {
1310 | continue;
1311 | }
1312 | if (def.remote in data) {
1313 | this[name] = data[def.remote];
1314 | } else if ('default' in def) {
1315 | this[name] = def["default"];
1316 | }
1317 | }
1318 | _ref = this.constructor.defaults;
1319 | for (name in _ref) {
1320 | value = _ref[name];
1321 | if (!(name in this)) {
1322 | this[name] = value;
1323 | }
1324 | }
1325 | }
1326 | if (this.constructor.idField in data) {
1327 | this.$id = obj[this.constructor.idField];
1328 | }
1329 | return this;
1330 | };
1331 |
1332 | return Resource;
1333 |
1334 | })();
1335 | return Resource;
1336 | }]);
1337 |
--------------------------------------------------------------------------------