├── 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 | [![Build Status](https://travis-ci.org/panta/angular-rest-orm.svg)](https://travis-ci.org/panta/angular-rest-orm) [![Bower version](https://badge.fury.io/bo/angular-rest-orm.svg)](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 | --------------------------------------------------------------------------------