diff --git a/.jshintrc b/.jshintrc index c40ad52..e408dcf 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,34 +1,39 @@ { - "node": false, - "browser": true, - "es5": true, - "esnext": true, - "bitwise": true, - "camelcase": true, - "curly": true, - "eqeqeq": true, - "immed": true, - "indent": 2, - "latedef": true, - "newcap": true, - "noarg": true, - "quotmark": "single", - "regexp": true, - "undef": true, - "unused": true, - "strict": true, - "trailing": true, - "smarttabs": true, - "predef": [ - "inject", - "describe", - "it", - "beforeEach", - "afterEach", - "assert", - "require", - "module", - "exports", - "angular" - ] + "node": false, + "browser": true, + "es5": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 2, + "latedef": true, + "newcap": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "unused": true, + "strict": false, + "trailing": true, + "smarttabs": true, + "globals": { + "inject": true, + "describe": true, + "it": true, + "beforeEach": true, + "afterEach": true, + "assert": true, + "startInjector": true, + "DS": true, + "fail": true, + "$httpBackend": true, + "console": true, + "require": true, + "module": true, + "exports": true, + "angular": true + } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 60bd3a7..faad59c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +##### 0.10.0 - 29 June 2014 + +###### Breaking API changes +- #76 - Queries and filtering. See [TRANSITION.md](https://github.com/jmdobry/angular-data/blob/master/TRANSITION.md). +- #82 - Simplify error handling. Reduced size of angular-data.min.js by 4kb. +- #42 - Relations/Associations. `DS.inject` now looks for relations and injects them as well. + +###### Backwards compatible API changes +- #17 - Where predicates should be able to handle OR, not just AND +- #23 - Computed Properties +- #78 - Added optional callback to `bindOne` and `bindAll` +- #79 - `ejectAll` should clear matching completed queries +- #83 - Implement `DS.loadRelations(resourceName, instance(Id), relations[, options])` +- #84 - idAttribute of a resource can be a computed property + ##### 0.9.1 - 30 May 2014 ###### Backwards compatible bug fixes diff --git a/Gruntfile.js b/Gruntfile.js index 8f690a6..2ec3e89 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -6,372 +6,375 @@ * Licensed under the MIT license. */ module.exports = function (grunt) { - 'use strict'; + 'use strict'; - require('load-grunt-tasks')(grunt); - require('time-grunt')(grunt); + require('load-grunt-tasks')(grunt); + require('time-grunt')(grunt); - var dev = process.cwd().indexOf('/home/jdobry/angular-data') === -1, - pkg = grunt.file.readJSON('package.json'); + var dev = process.cwd().indexOf('/home/jdobry/angular-data') === -1, + pkg = grunt.file.readJSON('package.json'); - // Project configuration. - grunt.initConfig({ - pkg: pkg, - clean: { - coverage: ['coverage/'], - dist: ['dist/'], - doc: ['doc/'], - afterDoc: [ - 'doc/resources/img/angular.png', - 'doc/resources/img/angular_grey.png', - 'doc/resources/img/AngularJS-small.png', - 'doc/resources/img/docular-small.png', - 'doc/resources/img/favicon.ico', - 'doc/resources/img/grunt.png', - 'doc/resources/img/grunt_grey.png', - 'doc/resources/img/node.png', - 'doc/resources/img/node_grey.png', - 'doc/resources/angular/', - 'doc/resources/doc_api_resources/doc_api.js', - 'doc/resources/js/docs*.js', - 'doc/resources/js/jquery*.js' - ] - }, - jshint: { - all: ['Gruntfile.js', 'src/**/*.js', 'test/*.js'], - jshintrc: '.jshintrc' - }, - watch: { - files: ['src/**/*.js'], - tasks: ['build'] - }, - uglify: { - main: { - options: { - banner: '/**\n' + - '* @author Jason Dobry \n' + - '* @file angular-data.min.js\n' + - '* @version <%= pkg.version %> - Homepage \n' + - '* @copyright (c) 2014 Jason Dobry \n' + - '* @license MIT \n' + - '*\n' + - '* @overview Data store for Angular.js.\n' + - '*/\n' - }, - files: { - 'dist/angular-data.min.js': ['dist/angular-data.js'] - } - }, - scripts: { - files: { - 'doc/resources/js/libs.min.js': ['doc/resources/js/libs.js'] - } - } - }, - browserify: { - dist: { - files: { - 'dist/angular-data.js': ['src/index.js'] - }, - options: { - // TODO: There's got to be a better way to consume observe-js without it polluting the global space - postBundleCB: function (err, src, next) { - if (err) { - next(err); - } - src = src.replace('(typeof global !== \'undefined\' && global ? global : window)', '((exports.Number = { isNaN: window.isNaN }) ? exports : exports)'); - next(err, src); - } - } - } - }, - karma: { - options: { - configFile: './karma.conf.js' - }, - dev: { - browsers: ['Chrome'], - autoWatch: true, - singleRun: false, - exclude: [ - 'test/integration/datastore/cacheFactory.test.js' - ] - }, - min: { - browsers: ['Firefox', 'PhantomJS'], - options: { - files: [ - 'bower_components/angular/angular.js', - 'bower_components/angular-mocks/angular-mocks.js', - 'bower_components/angular-cache/dist/angular-cache.js', - 'dist/angular-data.min.js', - 'karma.start.js', - 'test/integration/**/*.js' - ] - }, - exclude: [ - 'test/integration/datastore/cacheFactory.test.js' - ] - }, - cacheFactory: { - browsers: ['Firefox', 'PhantomJS'], - options: { - files: [ - 'bower_components/angular/angular.js', - 'bower_components/angular-mocks/angular-mocks.js', - 'dist/angular-data.js', - 'karma.start.js', - 'test/integration/datastore/cacheFactory.test.js' - ] - } - }, - ci: { - browsers: ['Firefox', 'PhantomJS'], - exclude: [ - 'test/integration/datastore/cacheFactory.test.js' - ] - } - }, - coveralls: { - options: { - coverage_dir: 'coverage' - } - }, + // Project configuration. + grunt.initConfig({ + pkg: pkg, + clean: { + coverage: ['coverage/'], + dist: ['dist/'], + doc: ['doc/'], + afterDoc: [ + 'doc/resources/img/angular.png', + 'doc/resources/img/angular_grey.png', + 'doc/resources/img/AngularJS-small.png', + 'doc/resources/img/docular-small.png', + 'doc/resources/img/favicon.ico', + 'doc/resources/img/grunt.png', + 'doc/resources/img/grunt_grey.png', + 'doc/resources/img/node.png', + 'doc/resources/img/node_grey.png', + 'doc/resources/angular/', + 'doc/resources/doc_api_resources/doc_api.js', + 'doc/resources/js/docs*.js', + 'doc/resources/js/jquery*.js' + ] + }, + jshint: { + all: ['Gruntfile.js', 'src/**/*.js', 'test/*.js'], + jshintrc: '.jshintrc' + }, + watch: { + files: ['src/**/*.js'], + tasks: ['build'] + }, + uglify: { + main: { + options: { + banner: '/**\n' + + '* @author Jason Dobry \n' + + '* @file angular-data.min.js\n' + + '* @version <%= pkg.version %> - Homepage \n' + + '* @copyright (c) 2014 Jason Dobry \n' + + '* @license MIT \n' + + '*\n' + + '* @overview Data store for Angular.js.\n' + + '*/\n' + }, + files: { + 'dist/angular-data.min.js': ['dist/angular-data.js'] + } + }, + scripts: { + files: { + 'doc/resources/js/libs.min.js': ['doc/resources/js/libs.js'] + } + } + }, + browserify: { + dist: { + files: { + 'dist/angular-data.js': ['src/index.js'] + }, + options: { + // TODO: There's got to be a better way to consume observe-js without it polluting the global space + postBundleCB: function (err, src, next) { + if (err) { + next(err); + } + src = src.replace('(typeof global !== \'undefined\' && global ? global : window)', '((exports.Number = { isNaN: window.isNaN }) ? exports : exports)'); + next(err, src); + } + } + } + }, + karma: { + options: { + configFile: './karma.conf.js' + }, + dev: { + browsers: ['Chrome'], + autoWatch: true, + singleRun: false, + reporters: ['spec'], + preprocessors: {}, + exclude: [ + 'test/integration/datastore/cacheFactory.test.js' + ] + }, + min: { + browsers: ['Firefox', 'PhantomJS'], + options: { + files: [ + 'bower_components/angular/angular.js', + 'bower_components/angular-mocks/angular-mocks.js', + 'bower_components/angular-cache/dist/angular-cache.js', + 'dist/angular-data.min.js', + 'karma.start.js', + 'test/integration/**/*.js' + ] + }, + exclude: [ + 'test/integration/datastore/cacheFactory.test.js' + ] + }, + cacheFactory: { + browsers: ['Firefox', 'PhantomJS'], + options: { + files: [ + 'bower_components/angular/angular.js', + 'bower_components/angular-mocks/angular-mocks.js', + 'dist/angular-data.js', + 'karma.start.js', + 'test/integration/datastore/cacheFactory.test.js' + ] + } + }, + ci: { + browsers: ['Firefox', 'PhantomJS'], + exclude: [ + 'test/integration/datastore/cacheFactory.test.js' + ] + } + }, + coveralls: { + options: { + coverage_dir: 'coverage' + } + }, - concat: { - libs: { - src: [ - 'doc/resources/js/jquery.js', - 'doc/resources/js/jquery.goto.js', - 'doc/resources/js/jquery.cookie.js', - 'doc/resources/angular/angular.js', - 'doc/resources/angular/angular-bootstrap.js', - 'doc/resources/angular/angular-bootstrap-prettify.js', - 'doc/resources/angular/angular-cookies.js', - 'doc/resources/angular/angular-resource.js', - 'doc/resources/angular/angular-sanitize.js' + concat: { + libs: { + src: [ + 'doc/resources/js/jquery.js', + 'doc/resources/js/jquery.goto.js', + 'doc/resources/js/jquery.cookie.js', + 'doc/resources/angular/angular.js', + 'doc/resources/angular/angular-bootstrap.js', + 'doc/resources/angular/angular-bootstrap-prettify.js', + 'doc/resources/angular/angular-cookies.js', + 'doc/resources/angular/angular-resource.js', + 'doc/resources/angular/angular-sanitize.js' - ], - dest: 'doc/resources/js/libs.js' - }, - scripts: { - src: [ - 'doc/resources/js/docs_module_begin.js', - 'guide/angular-data.js', - 'doc/resources/doc_api_resources/doc_api.js', - 'doc/resources/js/docs_module_end.js', - 'doc/documentation/docs-metadata.js', - 'doc/documentation/groups-metadata.js', - 'doc/documentation/layout-metadata.js' + ], + dest: 'doc/resources/js/libs.js' + }, + scripts: { + src: [ + 'doc/resources/js/docs_module_begin.js', + 'guide/angular-data.js', + 'doc/resources/doc_api_resources/doc_api.js', + 'doc/resources/js/docs_module_end.js', + 'doc/documentation/docs-metadata.js', + 'doc/documentation/groups-metadata.js', + 'doc/documentation/layout-metadata.js' - ], - dest: 'doc/resources/js/scripts.js' - }, - css: { - src: [ - 'doc/resources/css/bootstrap.min.css', - 'doc/resources/css/font-awesome.css', - 'doc/resources/css/docular.css', - 'doc/resources/css/custom.css', - 'doc/resources/doc_api_resources/doc_api.css', - 'guide/angular-data.css' - ], - dest: 'doc/resources/css/styles.css' - } - }, + ], + dest: 'doc/resources/js/scripts.js' + }, + css: { + src: [ + 'doc/resources/css/bootstrap.min.css', + 'doc/resources/css/font-awesome.css', + 'doc/resources/css/docular.css', + 'doc/resources/css/custom.css', + 'doc/resources/doc_api_resources/doc_api.css', + 'guide/angular-data.css' + ], + dest: 'doc/resources/css/styles.css' + } + }, - copy: { - favicon: { - expand: true, - cwd: 'guide/', - src: 'favicon.ico', - dest: 'doc/', - flatten: true - }, - index: { - expand: true, - cwd: 'guide/', - src: 'index.html', - dest: 'doc/', - flatten: true - }, - data_white: { - expand: true, - cwd: 'guide/', - src: 'data_white.png', - dest: 'doc/resources/img/', - flatten: true - }, - chart: { - expand: true, - cwd: 'guide/', - src: 'chart.png', - dest: 'doc/resources/img/', - flatten: true - }, - cream_dust: { - expand: true, - cwd: 'guide/', - src: 'cream_dust.png', - dest: 'doc/resources/img/', - flatten: true - } - }, - docular: { - groups: [ - { - groupTitle: 'Guide', - groupId: 'guide', - groupIcon: 'icon-book', - sections: [ - { - id: 'angular-data', - title: 'angular-data', - docs: [ - 'guide/angular-data/index.doc', - 'guide/angular-data/overview.doc', - 'guide/angular-data/resources.doc', - 'guide/angular-data/asynchronous.doc', - 'guide/angular-data/synchronous.doc', - 'guide/angular-data/queries.doc', - 'guide/angular-data/adapters.doc', - 'guide/angular-data/how.doc' - ], - rank: { - index: 1, - overview: 2, - resources: 3, - asynchronous: 4, - synchronous: 5, - queries: 6, - adapters: 7, - how: 8 - } - }, - { - id: 'angular-cache', - title: 'angular-cache', - docs: ['guide/angular-cache/'], - rank: { - index: 1, - basics: 2, - configure: 3, - http: 4, - storage: 5 - } - }, - { - id: 'angular-data-mocks', - title: 'angular-data-mocks', - docs: ['guide/angular-data-mocks/'], - rank: { - index: 1, - overview: 2, - setup: 3, - testing: 4 - } - }, - { - id: 'angular-data-resource', - title: 'Defining Resources', - docs: ['guide/angular-data/resource/'], - rank: { - index: 1, - overview: 2, - basic: 3, - advanced: 4, - lifecycle: 5, - custom: 6 - } - } - ] - }, - { - groupTitle: 'API', - groupId: 'api', - groupIcon: 'icon-wrench', - showSource: true, - sections: [ - { - id: 'angular-data', - title: 'angular-data', - scripts: [ - 'src/' - ], - docs: ['guide/api'] - }, - { - id: 'angular-cache', - title: 'angular-cache', - scripts: [ - 'bower_components/angular-cache/dist/angular-cache.js' - ], - docs: ['guide/api'] - }, - { - id: 'angular-data-mocks', - title: 'angular-data-mocks', - scripts: [ - 'bower_components/angular-data-mocks/dist/angular-data-mocks.js' - ], - docs: ['guide/api'] - } - ] - } - ], - docular_webapp_target: 'doc', - showDocularDocs: false, - showAngularDocs: false, - docular_partial_home: 'guide/home.html', - docular_partial_navigation: 'guide/nav.html', - docular_partial_footer: 'guide/footer.html', - analytics: { - account: 'UA-34445126-2', - domainName: 'angular-data.pseudobry.com' - }, - discussions: { - shortName: 'angular-data', - url: 'http://angular-data.pseudobry.com', - dev: dev - } - } - }); + copy: { + favicon: { + expand: true, + cwd: 'guide/', + src: 'favicon.ico', + dest: 'doc/', + flatten: true + }, + index: { + expand: true, + cwd: 'guide/', + src: 'index.html', + dest: 'doc/', + flatten: true + }, + data_white: { + expand: true, + cwd: 'guide/', + src: 'data_white.png', + dest: 'doc/resources/img/', + flatten: true + }, + chart: { + expand: true, + cwd: 'guide/', + src: 'chart.png', + dest: 'doc/resources/img/', + flatten: true + }, + cream_dust: { + expand: true, + cwd: 'guide/', + src: 'cream_dust.png', + dest: 'doc/resources/img/', + flatten: true + } + }, + docular: { + groups: [ + { + groupTitle: 'Guide', + groupId: 'guide', + groupIcon: 'icon-book', + sections: [ + { + id: 'angular-data', + title: 'angular-data', + docs: [ + 'guide/angular-data/index.md', + 'guide/angular-data/overview.md', + 'guide/angular-data/resources.md', + 'guide/angular-data/asynchronous.md', + 'guide/angular-data/synchronous.md', + 'guide/angular-data/queries.md', + 'guide/angular-data/adapters.md', + 'guide/angular-data/how.md' + ], + rank: { + index: 1, + overview: 2, + resources: 3, + asynchronous: 4, + synchronous: 5, + queries: 6, + adapters: 7, + how: 8 + } + }, + { + id: 'angular-cache', + title: 'angular-cache', + docs: ['guide/angular-cache/'], + rank: { + index: 1, + basics: 2, + configure: 3, + http: 4, + storage: 5 + } + }, + { + id: 'angular-data-mocks', + title: 'angular-data-mocks', + docs: ['guide/angular-data-mocks/'], + rank: { + index: 1, + overview: 2, + setup: 3, + testing: 4 + } + }, + { + id: 'angular-data-resource', + title: 'Defining Resources', + docs: ['guide/angular-data/resource/'], + rank: { + index: 1, + overview: 2, + basic: 3, + advanced: 4, + lifecycle: 5, + custom: 6, + relations: 7 + } + } + ] + }, + { + groupTitle: 'API', + groupId: 'api', + groupIcon: 'icon-wrench', + showSource: true, + sections: [ + { + id: 'angular-data', + title: 'angular-data', + scripts: [ + 'src/' + ], + docs: ['guide/api'] + }, + { + id: 'angular-cache', + title: 'angular-cache', + scripts: [ + 'bower_components/angular-cache/dist/angular-cache.js' + ], + docs: ['guide/api'] + }, + { + id: 'angular-data-mocks', + title: 'angular-data-mocks', + scripts: [ + 'bower_components/angular-data-mocks/dist/angular-data-mocks.js' + ], + docs: ['guide/api'] + } + ] + } + ], + docular_webapp_target: 'doc', + showDocularDocs: false, + showAngularDocs: false, + docular_partial_home: 'guide/home.html', + docular_partial_navigation: 'guide/nav.html', + docular_partial_footer: 'guide/footer.html', + analytics: { + account: 'UA-34445126-2', + domainName: 'angular-data.pseudobry.com' + }, + discussions: { + shortName: 'angular-data', + url: 'http://angular-data.pseudobry.com', + dev: dev + } + } + }); - grunt.registerTask('version', function (filePath) { - var file = grunt.file.read(filePath); + grunt.registerTask('version', function (filePath) { + var file = grunt.file.read(filePath); - file = file.replace(/<%= pkg\.version %>/gi, pkg.version); + file = file.replace(/<%= pkg\.version %>/gi, pkg.version); - grunt.file.write(filePath, file); - }); + grunt.file.write(filePath, file); + }); - grunt.registerTask('banner', function () { - var file = grunt.file.read('dist/angular-data.js'); + grunt.registerTask('banner', function () { + var file = grunt.file.read('dist/angular-data.js'); - var banner = '/**\n' + - '* @author Jason Dobry \n' + - '* @file angular-data.js\n' + - '* @version ' + pkg.version + ' - Homepage \n' + - '* @copyright (c) 2014 Jason Dobry \n' + - '* @license MIT \n' + - '*\n' + - '* @overview Data store for Angular.js.\n' + - '*/\n'; + var banner = '/**\n' + + '* @author Jason Dobry \n' + + '* @file angular-data.js\n' + + '* @version ' + pkg.version + ' - Homepage \n' + + '* @copyright (c) 2014 Jason Dobry \n' + + '* @license MIT \n' + + '*\n' + + '* @overview Data store for Angular.js.\n' + + '*/\n'; - file = banner + file; + file = banner + file; - grunt.file.write('dist/angular-data.js', file); - }); + grunt.file.write('dist/angular-data.js', file); + }); - grunt.registerTask('test', ['build', 'karma:ci', 'karma:cacheFactory', 'karma:min']); - grunt.registerTask('doc', ['clean:doc', 'docular', 'concat', 'copy', 'clean:afterDoc', 'uglify:scripts']); - grunt.registerTask('build', [ - 'clean', - 'jshint', - 'browserify', - 'banner', - 'uglify:main' - ]); - grunt.registerTask('go', ['build', 'watch']); - grunt.registerTask('default', ['build']); + grunt.registerTask('test', ['build', 'karma:ci', 'karma:cacheFactory', 'karma:min']); + grunt.registerTask('doc', ['clean:doc', 'docular', 'concat', 'copy', 'clean:afterDoc', 'uglify:scripts']); + grunt.registerTask('build', [ + 'clean', + 'jshint', + 'browserify', + 'banner', + 'uglify:main' + ]); + grunt.registerTask('go', ['build', 'watch']); + grunt.registerTask('default', ['build']); }; diff --git a/README.md b/README.md index a36ceba..5891181 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ __Data store for Angular.js.__ -__Latest Release:__ [0.9.1](http://angular-data.codetrain.io/) -__master:__ [0.9.1](http://angular-data-next.codetrain.io/) +__Latest Release:__ [0.9.1](http://angular-data.pseudobry.com/) +__master:__ [0.9.1](http://angular-data-next.pseudobry.com/) Angular-data is in a pre-1.0.0 development stage; the API is fluctuating, not a lot of tests yet, etc. diff --git a/TRANSITION.md b/TRANSITION.md new file mode 100644 index 0000000..3512475 --- /dev/null +++ b/TRANSITION.md @@ -0,0 +1,100 @@ +### 0.9.x. ---> 0.10.0 - 29 June 2014 + +#### Breaking API changes +##### #76 - Queries and filtering. + +###### Before +`IllegalArgumentError` has an `errors` field. + +###### After +`IllegalArgumentError` no longer has an `errors` field. + +###### Before +```javascript +DS.findAll('post', { + query: { + where: { + name: 'John' + } + } +}) +``` + +###### After +```javascript +DS.findAll('post', { + where: { + name: 'John' + } +}) +``` + +###### Before +```javascript +DS.filter('post', { + query: { + where: { + name: 'John' + } + } +}) +``` + +###### After +```javascript +DS.filter('post', { + where: { + name: 'John' + } +}) +``` + +###### Before +```javascript +// override how DS.filter handles the "where" clause +DSProvider.defaults.filter = function (resourceName, where, attrs) { + // return true to keep the item in the result set + // return false to exclude it +}; +``` + +###### After +```javascript +// override how DS.filter handles the "where", "skip", "limit" and "orderBy" clauses +DSProvider.defaults.filter = function (collection, resourceName, params, options) { + // examine params and + // decide whether to exclude items, skip items, start from an offset, or sort the items + + // see the [default implementation that ships with angular-data](https://github.com/jmdobry/angular-data/blob/master/src/datastore/index.js#L12) + // overriding this method is useful when our server only understands a certain + // params format and you want angular-data's filter to behave the same as your server + + // angular-data looks for the following fields: + // - where + // - skip (or offset) + // - limit + // - orderBy (or sort) + + // return the filtered collection +}; +``` + +###### Before +```javascript +DSHttpAdapter.defaults.queryTransform = function (resourceName, query) { + // the second argument was the "query" field of the "params" passed in the DSHttpAdapter method + // return the transformed query +}; +``` + +###### After +```javascript +// This is useful when you don't want to implement the filter method above +// and instead rely on angular-data's expectation of where, skip, limit, orderBy, etc. +// This transform is useful when you want to change the where, skip, limit, orderBy, etc. fields +// into something your server understands. +DSHttpAdapter.defaults.queryTransform = function (resourceName, params) { + // the second argument is now the whole "params" object passed in the DSHttpAdapter method + // return the transformed params +}; +``` diff --git a/bower.json b/bower.json index a3bcbc1..7dafcef 100644 --- a/bower.json +++ b/bower.json @@ -2,8 +2,8 @@ "author": "Jason Dobry", "name": "angular-data", "description": "Data store for Angular.js.", - "version": "0.9.1", - "homepage": "http://angular-data.codetrain.io/", + "version": "0.10.0", + "homepage": "http://angular-data.pseudobry.com/", "repository": { "type": "git", "url": "git://github.com/jmdobry/angular-data.git" diff --git a/dist/angular-data.js b/dist/angular-data.js index b2fde84..82d7702 100644 --- a/dist/angular-data.js +++ b/dist/angular-data.js @@ -1,7 +1,7 @@ /** * @author Jason Dobry * @file angular-data.js -* @version 0.9.1 - Homepage +* @version 0.10.0 - Homepage * @copyright (c) 2014 Jason Dobry * @license MIT * @@ -1206,429 +1206,423 @@ var toString = require('../lang/toString'); */ function DSHttpAdapterProvider() { - /** - * @doc property - * @id DSHttpAdapterProvider.properties:defaults - * @name defaults - * @description - * Default configuration for this adapter. - * - * Properties: - * - * - `{function}` - `queryTransform` - See [the guide](/documentation/guide/adapters/index). Default: No-op. - */ - var defaults = this.defaults = { - /** - * @doc property - * @id DSHttpAdapterProvider.properties:defaults.queryTransform - * @name defaults.queryTransform - * @description - * Transform the angular-data query to something your server understands. You might just do this on the server instead. - * - * @param {string} resourceName The name of the resource. - * @param {object} query Response object from `$http()`. - * @returns {*} Returns `query` as-is. - */ - queryTransform: function (resourceName, query) { - return query; - } - }; + /** + * @doc property + * @id DSHttpAdapterProvider.properties:defaults + * @name defaults + * @description + * Default configuration for this adapter. + * + * Properties: + * + * - `{function}` - `queryTransform` - See [the guide](/documentation/guide/adapters/index). Default: No-op. + */ + var defaults = this.defaults = { + /** + * @doc property + * @id DSHttpAdapterProvider.properties:defaults.queryTransform + * @name defaults.queryTransform + * @description + * Transform the angular-data query to something your server understands. You might just do this on the server instead. + * + * @param {string} resourceName The name of the resource. + * @param {object} params Params sent through from `$http()`. + * @returns {*} Returns `params` as-is. + */ + queryTransform: function (resourceName, params) { + return params; + } + }; - this.$get = ['$http', '$log', 'DSUtils', function ($http, $log, DSUtils) { + this.$get = ['$http', '$log', 'DSUtils', function ($http, $log, DSUtils) { - /** - * @doc interface - * @id DSHttpAdapter - * @name DSHttpAdapter - * @description - * Default adapter used by angular-data. This adapter uses AJAX and JSON to send/retrieve data to/from a server. - * Developers may provide custom adapters that implement the adapter interface. - */ - return { - /** - * @doc property - * @id DSHttpAdapter.properties:defaults - * @name defaults - * @description - * Reference to [DSHttpAdapterProvider.defaults](/documentation/api/api/DSHttpAdapterProvider.properties:defaults). - */ - defaults: defaults, - - /** - * @doc method - * @id DSHttpAdapter.methods:HTTP - * @name HTTP - * @description - * Wrapper for `$http()`. - * - * ## Signature: - * ```js - * DS.HTTP(config) - * ``` - * - * ## Example: - * - * ```js - * works the same as $http() - * ``` - * - * @param {object} config Configuration for the request. - * @returns {Promise} Promise produced by the `$q` service. - */ - HTTP: HTTP, - - /** - * @doc method - * @id DSHttpAdapter.methods:GET - * @name GET - * @description - * Wrapper for `$http.get()`. - * - * ## Signature: - * ```js - * DS.GET(url[, config]) - * ``` - * - * ## Example: - * - * ```js - * Works the same as $http.get() - * ``` - * - * @param {string} url The url of the request. - * @param {object=} config Configuration for the request. - * @returns {Promise} Promise produced by the `$q` service. - */ - GET: GET, - - /** - * @doc method - * @id DSHttpAdapter.methods:POST - * @name POST - * @description - * Wrapper for `$http.post()`. - * - * ## Signature: - * ```js - * DS.POST(url[, attrs][, config]) - * ``` - * - * ## Example: - * - * ```js - * Works the same as $http.post() - * ``` - * - * @param {string} url The url of the request. - * @param {object=} attrs Request payload. - * @param {object=} config Configuration for the request. - * @returns {Promise} Promise produced by the `$q` service. - */ - POST: POST, - - /** - * @doc method - * @id DSHttpAdapter.methods:PUT - * @name PUT - * @description - * Wrapper for `$http.put()`. - * - * ## Signature: - * ```js - * DS.PUT(url[, attrs][, config]) - * ``` - * - * ## Example: - * - * ```js - * Works the same as $http.put() - * ``` - * - * @param {string} url The url of the request. - * @param {object=} attrs Request payload. - * @param {object=} config Configuration for the request. - * @returns {Promise} Promise produced by the `$q` service. - */ - PUT: PUT, - - /** - * @doc method - * @id DSHttpAdapter.methods:DEL - * @name DEL - * @description - * Wrapper for `$http.delete()`. - * - * ## Signature: - * ```js - * DS.DEL(url[, config]) - * ``` - * - * ## Example: - * - * ```js - * Works the same as $http.delete - * ``` - * - * @param {string} url The url of the request. - * @param {object} config Configuration for the request. - * @returns {Promise} Promise produced by the `$q` service. - */ - DEL: DEL, - - /** - * @doc method - * @id DSHttpAdapter.methods:find - * @name find - * @description - * Retrieve a single entity from the server. - * - * Sends a `GET` request to `:baseUrl/:endpoint/:id`. - * - * @param {object} resourceConfig Properties: - * - `{string}` - `baseUrl` - Base url. - * - `{string=}` - `endpoint` - Endpoint path for the resource. - * @param {string|number} id The primary key of the entity to retrieve. - * @param {object=} options Optional configuration. Refer to the documentation for `$http.get`. - * @returns {Promise} Promise. - */ - find: find, - - /** - * @doc method - * @id DSHttpAdapter.methods:findAll - * @name findAll - * @description - * Retrieve a collection of entities from the server. - * - * Sends a `GET` request to `:baseUrl/:endpoint`. - * - * - * @param {object} resourceConfig Properties: - * - `{string}` - `baseUrl` - Base url. - * - `{string=}` - `endpoint` - Endpoint path for the resource. - * @param {object=} params Search query parameters. See the [query guide](/documentation/guide/queries/index). - * @param {object=} options Optional configuration. Refer to the documentation for `$http.get`. - * @returns {Promise} Promise. - */ - findAll: findAll, - - /** - * @doc method - * @id DSHttpAdapter.methods:findAll - * @name find - * @description - * Create a new entity on the server. - * - * Sends a `POST` request to `:baseUrl/:endpoint`. - * - * @param {object} resourceConfig Properties: - * - `{string}` - `baseUrl` - Base url. - * - `{string=}` - `endpoint` - Endpoint path for the resource. - * @param {object=} params Search query parameters. See the [query guide](/documentation/guide/queries/index). - * @param {object=} options Optional configuration. Refer to the documentation for `$http.post`. - * @returns {Promise} Promise. - */ - create: create, - - /** - * @doc method - * @id DSHttpAdapter.methods:update - * @name update - * @description - * Update an entity on the server. - * - * Sends a `PUT` request to `:baseUrl/:endpoint/:id`. - * - * @param {object} resourceConfig Properties: - * - `{string}` - `baseUrl` - Base url. - * - `{string=}` - `endpoint` - Endpoint path for the resource. - * @param {string|number} id The primary key of the entity to update. - * @param {object} attrs The attributes with which to update the entity. See the [query guide](/documentation/guide/queries/index). - * @param {object=} options Optional configuration. Refer to the documentation for `$http.put`. - * @returns {Promise} Promise. - */ - update: update, - - /** - * @doc method - * @id DSHttpAdapter.methods:updateAll - * @name updateAll - * @description - * Update a collection of entities on the server. - * - * Sends a `PUT` request to `:baseUrl/:endpoint`. - * - * - * @param {object} resourceConfig Properties: - * - `{string}` - `baseUrl` - Base url. - * - `{string=}` - `endpoint` - Endpoint path for the resource. - * @param {object=} params Search query parameters. See the [query guide](/documentation/guide/queries/index). - * @param {object=} options Optional configuration. Refer to the documentation for `$http.put`. - * @returns {Promise} Promise. - */ - updateAll: updateAll, - - /** - * @doc method - * @id DSHttpAdapter.methods:destroy - * @name destroy - * @description - * destroy an entity on the server. - * - * Sends a `PUT` request to `:baseUrl/:endpoint/:id`. - * - * @param {object} resourceConfig Properties: - * - `{string}` - `baseUrl` - Base url. - * - `{string=}` - `endpoint` - Endpoint path for the resource. - * @param {string|number} id The primary key of the entity to destroy. - * @param {object=} options Optional configuration. Refer to the documentation for `$http.delete`. - * @returns {Promise} Promise. - */ - destroy: destroy, - - /** - * @doc method - * @id DSHttpAdapter.methods:destroyAll - * @name destroyAll - * @description - * Retrieve a collection of entities from the server. - * - * Sends a `DELETE` request to `:baseUrl/:endpoint`. - * - * - * @param {object} resourceConfig Properties: - * - `{string}` - `baseUrl` - Base url. - * - `{string=}` - `endpoint` - Endpoint path for the resource. - * @param {object=} params Search query parameters. See the [query guide](/documentation/guide/queries/index). - * @param {object=} options Optional configuration. Refer to the documentation for `$http.delete`. - * @returns {Promise} Promise. - */ - destroyAll: destroyAll - }; + /** + * @doc interface + * @id DSHttpAdapter + * @name DSHttpAdapter + * @description + * Default adapter used by angular-data. This adapter uses AJAX and JSON to send/retrieve data to/from a server. + * Developers may provide custom adapters that implement the adapter interface. + */ + return { + /** + * @doc property + * @id DSHttpAdapter.properties:defaults + * @name defaults + * @description + * Reference to [DSHttpAdapterProvider.defaults](/documentation/api/api/DSHttpAdapterProvider.properties:defaults). + */ + defaults: defaults, + + /** + * @doc method + * @id DSHttpAdapter.methods:HTTP + * @name HTTP + * @description + * Wrapper for `$http()`. + * + * ## Signature: + * ```js + * DS.HTTP(config) + * ``` + * + * ## Example: + * + * ```js + * works the same as $http() + * ``` + * + * @param {object} config Configuration for the request. + * @returns {Promise} Promise produced by the `$q` service. + */ + HTTP: HTTP, + + /** + * @doc method + * @id DSHttpAdapter.methods:GET + * @name GET + * @description + * Wrapper for `$http.get()`. + * + * ## Signature: + * ```js + * DS.GET(url[, config]) + * ``` + * + * ## Example: + * + * ```js + * Works the same as $http.get() + * ``` + * + * @param {string} url The url of the request. + * @param {object=} config Configuration for the request. + * @returns {Promise} Promise produced by the `$q` service. + */ + GET: GET, + + /** + * @doc method + * @id DSHttpAdapter.methods:POST + * @name POST + * @description + * Wrapper for `$http.post()`. + * + * ## Signature: + * ```js + * DS.POST(url[, attrs][, config]) + * ``` + * + * ## Example: + * + * ```js + * Works the same as $http.post() + * ``` + * + * @param {string} url The url of the request. + * @param {object=} attrs Request payload. + * @param {object=} config Configuration for the request. + * @returns {Promise} Promise produced by the `$q` service. + */ + POST: POST, + + /** + * @doc method + * @id DSHttpAdapter.methods:PUT + * @name PUT + * @description + * Wrapper for `$http.put()`. + * + * ## Signature: + * ```js + * DS.PUT(url[, attrs][, config]) + * ``` + * + * ## Example: + * + * ```js + * Works the same as $http.put() + * ``` + * + * @param {string} url The url of the request. + * @param {object=} attrs Request payload. + * @param {object=} config Configuration for the request. + * @returns {Promise} Promise produced by the `$q` service. + */ + PUT: PUT, + + /** + * @doc method + * @id DSHttpAdapter.methods:DEL + * @name DEL + * @description + * Wrapper for `$http.delete()`. + * + * ## Signature: + * ```js + * DS.DEL(url[, config]) + * ``` + * + * ## Example: + * + * ```js + * Works the same as $http.delete + * ``` + * + * @param {string} url The url of the request. + * @param {object} config Configuration for the request. + * @returns {Promise} Promise produced by the `$q` service. + */ + DEL: DEL, + + /** + * @doc method + * @id DSHttpAdapter.methods:find + * @name find + * @description + * Retrieve a single entity from the server. + * + * Sends a `GET` request to `:baseUrl/:endpoint/:id`. + * + * @param {object} resourceConfig Properties: + * - `{string}` - `baseUrl` - Base url. + * - `{string=}` - `endpoint` - Endpoint path for the resource. + * @param {string|number} id The primary key of the entity to retrieve. + * @param {object=} options Optional configuration. Refer to the documentation for `$http.get`. + * @returns {Promise} Promise. + */ + find: find, + + /** + * @doc method + * @id DSHttpAdapter.methods:findAll + * @name findAll + * @description + * Retrieve a collection of entities from the server. + * + * Sends a `GET` request to `:baseUrl/:endpoint`. + * + * + * @param {object} resourceConfig Properties: + * - `{string}` - `baseUrl` - Base url. + * - `{string=}` - `endpoint` - Endpoint path for the resource. + * @param {object=} params Search query parameters. See the [query guide](/documentation/guide/queries/index). + * @param {object=} options Optional configuration. Refer to the documentation for `$http.get`. + * @returns {Promise} Promise. + */ + findAll: findAll, + + /** + * @doc method + * @id DSHttpAdapter.methods:findAll + * @name find + * @description + * Create a new entity on the server. + * + * Sends a `POST` request to `:baseUrl/:endpoint`. + * + * @param {object} resourceConfig Properties: + * - `{string}` - `baseUrl` - Base url. + * - `{string=}` - `endpoint` - Endpoint path for the resource. + * @param {object=} params Search query parameters. See the [query guide](/documentation/guide/queries/index). + * @param {object=} options Optional configuration. Refer to the documentation for `$http.post`. + * @returns {Promise} Promise. + */ + create: create, + + /** + * @doc method + * @id DSHttpAdapter.methods:update + * @name update + * @description + * Update an entity on the server. + * + * Sends a `PUT` request to `:baseUrl/:endpoint/:id`. + * + * @param {object} resourceConfig Properties: + * - `{string}` - `baseUrl` - Base url. + * - `{string=}` - `endpoint` - Endpoint path for the resource. + * @param {string|number} id The primary key of the entity to update. + * @param {object} attrs The attributes with which to update the entity. See the [query guide](/documentation/guide/queries/index). + * @param {object=} options Optional configuration. Refer to the documentation for `$http.put`. + * @returns {Promise} Promise. + */ + update: update, + + /** + * @doc method + * @id DSHttpAdapter.methods:updateAll + * @name updateAll + * @description + * Update a collection of entities on the server. + * + * Sends a `PUT` request to `:baseUrl/:endpoint`. + * + * + * @param {object} resourceConfig Properties: + * - `{string}` - `baseUrl` - Base url. + * - `{string=}` - `endpoint` - Endpoint path for the resource. + * @param {object=} params Search query parameters. See the [query guide](/documentation/guide/queries/index). + * @param {object=} options Optional configuration. Refer to the documentation for `$http.put`. + * @returns {Promise} Promise. + */ + updateAll: updateAll, + + /** + * @doc method + * @id DSHttpAdapter.methods:destroy + * @name destroy + * @description + * destroy an entity on the server. + * + * Sends a `PUT` request to `:baseUrl/:endpoint/:id`. + * + * @param {object} resourceConfig Properties: + * - `{string}` - `baseUrl` - Base url. + * - `{string=}` - `endpoint` - Endpoint path for the resource. + * @param {string|number} id The primary key of the entity to destroy. + * @param {object=} options Optional configuration. Refer to the documentation for `$http.delete`. + * @returns {Promise} Promise. + */ + destroy: destroy, + + /** + * @doc method + * @id DSHttpAdapter.methods:destroyAll + * @name destroyAll + * @description + * Retrieve a collection of entities from the server. + * + * Sends a `DELETE` request to `:baseUrl/:endpoint`. + * + * + * @param {object} resourceConfig Properties: + * - `{string}` - `baseUrl` - Base url. + * - `{string=}` - `endpoint` - Endpoint path for the resource. + * @param {object=} params Search query parameters. See the [query guide](/documentation/guide/queries/index). + * @param {object=} options Optional configuration. Refer to the documentation for `$http.delete`. + * @returns {Promise} Promise. + */ + destroyAll: destroyAll + }; - function HTTP(config) { - var start = new Date().getTime(); + function HTTP(config) { + var start = new Date().getTime(); - return $http(config).then(function (data) { - $log.debug(data.config.method + ' request:' + data.config.url + ' Time taken: ' + (new Date().getTime() - start) + 'ms', arguments); - return data; - }); - } + return $http(config).then(function (data) { + $log.debug(data.config.method + ' request:' + data.config.url + ' Time taken: ' + (new Date().getTime() - start) + 'ms', arguments); + return data; + }); + } - function GET(url, config) { - config = config || {}; - return HTTP(DSUtils.deepMixIn(config, { - url: url, - method: 'GET' - })); - } + function GET(url, config) { + config = config || {}; + return HTTP(DSUtils.deepMixIn(config, { + url: url, + method: 'GET' + })); + } - function POST(url, attrs, config) { - config = config || {}; - return HTTP(DSUtils.deepMixIn(config, { - url: url, - data: attrs, - method: 'POST' - })); - } + function POST(url, attrs, config) { + config = config || {}; + return HTTP(DSUtils.deepMixIn(config, { + url: url, + data: attrs, + method: 'POST' + })); + } - function PUT(url, attrs, config) { - config = config || {}; - return HTTP(DSUtils.deepMixIn(config, { - url: url, - data: attrs, - method: 'PUT' - })); - } + function PUT(url, attrs, config) { + config = config || {}; + return HTTP(DSUtils.deepMixIn(config, { + url: url, + data: attrs, + method: 'PUT' + })); + } - function DEL(url, config) { - config = config || {}; - return this.HTTP(DSUtils.deepMixIn(config, { - url: url, - method: 'DELETE' - })); - } + function DEL(url, config) { + config = config || {}; + return this.HTTP(DSUtils.deepMixIn(config, { + url: url, + method: 'DELETE' + })); + } - function create(resourceConfig, attrs, options) { - options = options || {}; - return this.POST( - DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint), - attrs, - options - ); - } + function create(resourceConfig, attrs, options) { + options = options || {}; + return this.POST( + DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint), + attrs, + options + ); + } - function destroy(resourceConfig, id, options) { - options = options || {}; - return this.DEL( - DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id), - options - ); - } + function destroy(resourceConfig, id, options) { + options = options || {}; + return this.DEL( + DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id), + options + ); + } - function destroyAll(resourceConfig, params, options) { - options = options || {}; - options.params = options.params || {}; - if (params) { - if (params.query) { - params.query = defaults.queryTransform(resourceConfig.name, params.query); - } - DSUtils.deepMixIn(options.params, params); - } - return this.DEL( - DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint), - options - ); - } + function destroyAll(resourceConfig, params, options) { + options = options || {}; + options.params = options.params || {}; + if (params) { + params = defaults.queryTransform(resourceConfig.name, params); + DSUtils.deepMixIn(options.params, params); + } + return this.DEL( + DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint), + options + ); + } - function find(resourceConfig, id, options) { - options = options || {}; - return this.GET( - DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id), - options - ); - } + function find(resourceConfig, id, options) { + options = options || {}; + return this.GET( + DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id), + options + ); + } - function findAll(resourceConfig, params, options) { - options = options || {}; - options.params = options.params || {}; - if (params) { - if (params.query) { - params.query = defaults.queryTransform(resourceConfig.name, params.query); - } - DSUtils.deepMixIn(options.params, params); - } - return this.GET( - DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint), - options - ); - } + function findAll(resourceConfig, params, options) { + options = options || {}; + options.params = options.params || {}; + if (params) { + params = defaults.queryTransform(resourceConfig.name, params); + DSUtils.deepMixIn(options.params, params); + } + return this.GET( + DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint), + options + ); + } - function update(resourceConfig, id, attrs, options) { - options = options || {}; - return this.PUT( - DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id), - attrs, - options - ); - } + function update(resourceConfig, id, attrs, options) { + options = options || {}; + return this.PUT( + DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id), + attrs, + options + ); + } - function updateAll(resourceConfig, attrs, params, options) { - options = options || {}; - options.params = options.params || {}; - if (params) { - if (params.query) { - params.query = defaults.queryTransform(resourceConfig.name, params.query); - } - DSUtils.deepMixIn(options.params, params); - } - return this.PUT( - DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint), - attrs, - options - ); - } - }]; + function updateAll(resourceConfig, attrs, params, options) { + options = options || {}; + options.params = options.params || {}; + if (params) { + params = defaults.queryTransform(resourceConfig.name, params); + DSUtils.deepMixIn(options.params, params); + } + return this.PUT( + DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint), + attrs, + options + ); + } + }]; } module.exports = DSHttpAdapterProvider; @@ -1641,179 +1635,179 @@ module.exports = DSHttpAdapterProvider; */ function DSLocalStorageProvider() { - this.$get = ['$q', 'DSUtils', function ($q, DSUtils) { + this.$get = ['$q', 'DSUtils', function ($q, DSUtils) { - /** - * @doc interface - * @id DSLocalStorage - * @name DSLocalStorage - * @description - * Default adapter used by angular-data. This adapter uses AJAX and JSON to send/retrieve data to/from a server. - * Developers may provide custom adapters that implement the adapter interface. - */ - return { - /** - * @doc method - * @id DSLocalStorage.methods:find - * @name find - * @description - * Retrieve a single entity from localStorage. - * - * Calls `localStorage.getItem(key)`. - * - * @param {object} resourceConfig Properties: - * - `{string}` - `baseUrl` - Base url. - * - `{string=}` - `namespace` - Namespace path for the resource. - * @param {string|number} id The primary key of the entity to retrieve. - * @returns {Promise} Promise. - */ - find: find, - - /** - * @doc method - * @id DSLocalStorage.methods:findAll - * @name findAll - * @description - * Not supported. - */ - findAll: function () { - throw new Error('Not supported!'); - }, - - /** - * @doc method - * @id DSLocalStorage.methods:findAll - * @name find - * @description - * Not supported. - */ - create: function () { - throw new Error('Not supported!'); - }, - - /** - * @doc method - * @id DSLocalStorage.methods:update - * @name update - * @description - * Update an entity in localStorage. - * - * Calls `localStorage.setItem(key, value)`. - * - * @param {object} resourceConfig Properties: - * - `{string}` - `baseUrl` - Base url. - * - `{string=}` - `namespace` - Namespace path for the resource. - * @param {string|number} id The primary key of the entity to update. - * @param {object} attrs The attributes with which to update the entity. - * @returns {Promise} Promise. - */ - update: update, - - /** - * @doc method - * @id DSLocalStorage.methods:updateAll - * @name updateAll - * @description - * Not supported. - */ - updateAll: function () { - throw new Error('Not supported!'); - }, - - /** - * @doc method - * @id DSLocalStorage.methods:destroy - * @name destroy - * @description - * Destroy an entity from localStorage. - * - * Calls `localStorage.removeItem(key)`. - * - * @param {object} resourceConfig Properties: - * - `{string}` - `baseUrl` - Base url. - * - `{string=}` - `endpoint` - Endpoint path for the resource. - * @param {string|number} id The primary key of the entity to destroy. - * @returns {Promise} Promise. - */ - destroy: destroy, - - /** - * @doc method - * @id DSLocalStorage.methods:destroyAll - * @name destroyAll - * @description - * Not supported. - */ - destroyAll: function () { - throw new Error('Not supported!'); - } - }; + /** + * @doc interface + * @id DSLocalStorage + * @name DSLocalStorage + * @description + * Default adapter used by angular-data. This adapter uses AJAX and JSON to send/retrieve data to/from a server. + * Developers may provide custom adapters that implement the adapter interface. + */ + return { + /** + * @doc method + * @id DSLocalStorage.methods:find + * @name find + * @description + * Retrieve a single entity from localStorage. + * + * Calls `localStorage.getItem(key)`. + * + * @param {object} resourceConfig Properties: + * - `{string}` - `baseUrl` - Base url. + * - `{string=}` - `namespace` - Namespace path for the resource. + * @param {string|number} id The primary key of the entity to retrieve. + * @returns {Promise} Promise. + */ + find: find, + + /** + * @doc method + * @id DSLocalStorage.methods:findAll + * @name findAll + * @description + * Not supported. + */ + findAll: function () { + throw new Error('Not supported!'); + }, + + /** + * @doc method + * @id DSLocalStorage.methods:findAll + * @name find + * @description + * Not supported. + */ + create: function () { + throw new Error('Not supported!'); + }, + + /** + * @doc method + * @id DSLocalStorage.methods:update + * @name update + * @description + * Update an entity in localStorage. + * + * Calls `localStorage.setItem(key, value)`. + * + * @param {object} resourceConfig Properties: + * - `{string}` - `baseUrl` - Base url. + * - `{string=}` - `namespace` - Namespace path for the resource. + * @param {string|number} id The primary key of the entity to update. + * @param {object} attrs The attributes with which to update the entity. + * @returns {Promise} Promise. + */ + update: update, + + /** + * @doc method + * @id DSLocalStorage.methods:updateAll + * @name updateAll + * @description + * Not supported. + */ + updateAll: function () { + throw new Error('Not supported!'); + }, + + /** + * @doc method + * @id DSLocalStorage.methods:destroy + * @name destroy + * @description + * Destroy an entity from localStorage. + * + * Calls `localStorage.removeItem(key)`. + * + * @param {object} resourceConfig Properties: + * - `{string}` - `baseUrl` - Base url. + * - `{string=}` - `endpoint` - Endpoint path for the resource. + * @param {string|number} id The primary key of the entity to destroy. + * @returns {Promise} Promise. + */ + destroy: destroy, + + /** + * @doc method + * @id DSLocalStorage.methods:destroyAll + * @name destroyAll + * @description + * Not supported. + */ + destroyAll: function () { + throw new Error('Not supported!'); + } + }; - function GET(key) { - var deferred = $q.defer(); - try { - var item = localStorage.getItem(key); - deferred.resolve(item ? angular.fromJson(item) : undefined); - } catch (err) { - deferred.reject(err); - } - return deferred.promise; - } + function GET(key) { + var deferred = $q.defer(); + try { + var item = localStorage.getItem(key); + deferred.resolve(item ? angular.fromJson(item) : undefined); + } catch (err) { + deferred.reject(err); + } + return deferred.promise; + } - function PUT(key, value) { - var deferred = $q.defer(); - try { - var item = localStorage.getItem(key); - if (item) { - item = angular.fromJson(item); - DSUtils.deepMixIn(item, value); - deferred.resolve(localStorage.setItem(key, angular.toJson(item))); - } else { - deferred.resolve(localStorage.setItem(key, angular.toJson(value))); - } - } catch (err) { - deferred.reject(err); - } - return deferred.promise; - } + function PUT(key, value) { + var deferred = $q.defer(); + try { + var item = localStorage.getItem(key); + if (item) { + item = angular.fromJson(item); + DSUtils.deepMixIn(item, value); + deferred.resolve(localStorage.setItem(key, angular.toJson(item))); + } else { + deferred.resolve(localStorage.setItem(key, angular.toJson(value))); + } + } catch (err) { + deferred.reject(err); + } + return deferred.promise; + } - function DEL(key) { - var deferred = $q.defer(); - try { - deferred.resolve(localStorage.removeItem(key)); - } catch (err) { - deferred.reject(err); - } - return deferred.promise; - } + function DEL(key) { + var deferred = $q.defer(); + try { + deferred.resolve(localStorage.removeItem(key)); + } catch (err) { + deferred.reject(err); + } + return deferred.promise; + } - function destroy(resourceConfig, id, options) { - options = options || {}; - return DEL( - DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id), - options - ); - } + function destroy(resourceConfig, id, options) { + options = options || {}; + return DEL( + DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id), + options + ); + } - function find(resourceConfig, id, options) { - options = options || {}; - return GET( - DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id), - options - ); - } + function find(resourceConfig, id, options) { + options = options || {}; + return GET( + DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id), + options + ); + } - function update(resourceConfig, id, attrs, options) { - options = options || {}; - return PUT( - DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id), - attrs, - options - ).then(function () { - return GET(DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id)); - }); - } - }]; + function update(resourceConfig, id, attrs, options) { + options = options || {}; + return PUT( + DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id), + attrs, + options + ).then(function () { + return GET(DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id)); + }); + } + }]; } module.exports = DSLocalStorageProvider; @@ -1860,59 +1854,57 @@ var errorPrefix = 'DS.create(resourceName, attrs[, options]): '; * ## Rejects with: * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` */ function create(resourceName, attrs, options) { - var deferred = this.$q.defer(), - promise = deferred.promise; - - options = options || {}; - - if (!this.definitions[resourceName]) { - deferred.reject(new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!')); - } else if (!this.utils.isObject(attrs)) { - deferred.reject(new this.errors.IllegalArgumentError(errorPrefix + 'attrs: Must be an object!', { attrs: { actual: typeof attrs, expected: 'object' } })); - } else { - try { - var definition = this.definitions[resourceName], - resource = this.store[resourceName], - _this = this; - - promise = promise - .then(function (attrs) { - return _this.$q.promisify(definition.beforeValidate)(resourceName, attrs); - }) - .then(function (attrs) { - return _this.$q.promisify(definition.validate)(resourceName, attrs); - }) - .then(function (attrs) { - return _this.$q.promisify(definition.afterValidate)(resourceName, attrs); - }) - .then(function (attrs) { - return _this.$q.promisify(definition.beforeCreate)(resourceName, attrs); - }) - .then(function (attrs) { - return _this.adapters[options.adapter || definition.defaultAdapter].create(definition, definition.serialize(resourceName, attrs), options); - }) - .then(function (res) { - return _this.$q.promisify(definition.afterCreate)(resourceName, definition.deserialize(resourceName, res)); - }) - .then(function (data) { - var created = _this.inject(definition.name, data); - var id = created[definition.idAttribute]; - resource.previousAttributes[id] = _this.utils.deepMixIn({}, created); - resource.saved[id] = _this.utils.updateTimestamp(resource.saved[id]); - return _this.get(definition.name, id); - }); - - deferred.resolve(attrs); - } catch (err) { - deferred.reject(new this.errors.UnhandledError(err)); - } - } + var deferred = this.$q.defer(); + var promise = deferred.promise; - return promise; + try { + options = options || {}; + + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isObject(attrs)) { + throw new this.errors.IA(errorPrefix + 'attrs: Must be an object!'); + } + var definition = this.definitions[resourceName]; + var resource = this.store[resourceName]; + var _this = this; + + promise = promise + .then(function (attrs) { + return _this.$q.promisify(definition.beforeValidate)(resourceName, attrs); + }) + .then(function (attrs) { + return _this.$q.promisify(definition.validate)(resourceName, attrs); + }) + .then(function (attrs) { + return _this.$q.promisify(definition.afterValidate)(resourceName, attrs); + }) + .then(function (attrs) { + return _this.$q.promisify(definition.beforeCreate)(resourceName, attrs); + }) + .then(function (attrs) { + return _this.adapters[options.adapter || definition.defaultAdapter].create(definition, definition.serialize(resourceName, attrs), options); + }) + .then(function (res) { + return _this.$q.promisify(definition.afterCreate)(resourceName, definition.deserialize(resourceName, res)); + }) + .then(function (data) { + var created = _this.inject(definition.name, data); + var id = created[definition.idAttribute]; + resource.previousAttributes[id] = _this.utils.deepMixIn({}, created); + resource.saved[id] = _this.utils.updateTimestamp(resource.saved[id]); + return _this.get(definition.name, id); + }); + + deferred.resolve(attrs); + } catch (err) { + deferred.reject(err); + } + + return promise; } module.exports = create; @@ -1960,46 +1952,49 @@ var errorPrefix = 'DS.destroy(resourceName, id): '; * * - `{IllegalArgumentError}` * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` */ function destroy(resourceName, id, options) { - var deferred = this.$q.defer(), - promise = deferred.promise; - - options = options || {}; - - if (!this.definitions[resourceName]) { - deferred.reject(new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!')); - } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { - deferred.reject(new this.errors.IllegalArgumentError(errorPrefix + 'id: Must be a string or a number!', { id: { actual: typeof id, expected: 'string|number' } })); - } else { - var item = this.get(resourceName, id); - if (!item) { - deferred.reject(new this.errors.RuntimeError(errorPrefix + 'id: "' + id + '" not found!')); - } else { - var definition = this.definitions[resourceName], - resource = this.store[resourceName], - _this = this; - - promise = promise - .then(function (attrs) { - return _this.$q.promisify(definition.beforeDestroy)(resourceName, attrs); - }) - .then(function () { - return _this.adapters[options.adapter || definition.defaultAdapter].destroy(definition, id, options); - }) - .then(function () { - return _this.$q.promisify(definition.afterDestroy)(resourceName, item); - }) - .then(function () { - _this.eject(resourceName, id); - return id; - }); - deferred.resolve(item); - } - } + var deferred = this.$q.defer(); + var promise = deferred.promise; + + try { + options = options || {}; + + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { + throw new this.errors.IA(errorPrefix + 'id: Must be a string or a number!'); + } - return promise; + var item = this.get(resourceName, id); + if (!item) { + throw new this.errors.R(errorPrefix + 'id: "' + id + '" not found!'); + } + + var definition = this.definitions[resourceName]; + var _this = this; + + promise = promise + .then(function (attrs) { + return _this.$q.promisify(definition.beforeDestroy)(resourceName, attrs); + }) + .then(function () { + return _this.adapters[options.adapter || definition.defaultAdapter].destroy(definition, id, options); + }) + .then(function () { + return _this.$q.promisify(definition.afterDestroy)(resourceName, item); + }) + .then(function () { + _this.eject(resourceName, id); + return id; + }); + deferred.resolve(item); + } catch (err) { + deferred.reject(err); + } + + return promise; } module.exports = destroy; @@ -2023,7 +2018,7 @@ var errorPrefix = 'DS.destroyAll(resourceName, params[, options]): '; * ## Example: * * ```js - * var query = { + * var params = { * where: { * author: { * '==': 'John Anderson' @@ -2031,13 +2026,9 @@ var errorPrefix = 'DS.destroyAll(resourceName, params[, options]): '; * } * }; * - * DS.destroyAll('document', { - * query: query - * }).then(function (documents) { + * DS.destroyAll('document', params).then(function (documents) { * // The documents are gone from the data store - * DS.filter('document', { - * query: query - * }); // [] + * DS.filter('document', params); // [] * * }, function (err) { * // handle error @@ -2047,11 +2038,11 @@ var errorPrefix = 'DS.destroyAll(resourceName, params[, options]): '; * @param {string} resourceName The resource type, e.g. 'user', 'comment', etc. * @param {object} params Parameter object that is serialized into the query string. Properties: * - * - `{object=}` - `query` - The query object by which to filter items of the type specified by `resourceName`. Properties: - * - `{object=}` - `where` - Where clause. - * - `{number=}` - `limit` - Limit clause. - * - `{skip=}` - `skip` - Skip clause. - * - `{orderBy=}` - `orderBy` - OrderBy clause. + * - `{object=}` - `where` - Where clause. + * - `{number=}` - `limit` - Limit clause. + * - `{number=}` - `skip` - Skip clause. + * - `{number=}` - `offset` - Same as skip. + * - `{string|array=}` - `orderBy` - OrderBy clause. * * @param {object=} options Optional configuration. Properties: * - `{boolean=}` - `bypassCache` - Bypass the cache. Default: `false`. @@ -2061,40 +2052,41 @@ var errorPrefix = 'DS.destroyAll(resourceName, params[, options]): '; * ## Rejects with: * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` */ function destroyAll(resourceName, params, options) { - var deferred = this.$q.defer(), - promise = deferred.promise, - _this = this; - - options = options || {}; - - if (!this.definitions[resourceName]) { - deferred.reject(new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!')); - } else if (!this.utils.isObject(params)) { - deferred.reject(new this.errors.IllegalArgumentError(errorPrefix + 'params: Must be an object!')); - } else if (!this.utils.isObject(options)) { - deferred.reject(new this.errors.IllegalArgumentError(errorPrefix + 'options: Must be an object!')); - } else { - try { - var definition = this.definitions[resourceName]; - - promise = promise - .then(function () { - return _this.adapters[options.adapter || definition.defaultAdapter].destroyAll(definition, params, options); - }) - .then(function () { - return _this.ejectAll(resourceName, params); - }); - deferred.resolve(); - } catch (err) { - deferred.reject(new this.errors.UnhandledError(err)); - } - } + var deferred = this.$q.defer(); + var promise = deferred.promise; + + try { + var _this = this; + var IA = this.errors.IA; - return promise; + options = options || {}; + + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isObject(params)) { + throw new IA(errorPrefix + 'params: Must be an object!'); + } else if (!this.utils.isObject(options)) { + throw new IA(errorPrefix + 'options: Must be an object!'); + } + + var definition = this.definitions[resourceName]; + + promise = promise + .then(function () { + return _this.adapters[options.adapter || definition.defaultAdapter].destroyAll(definition, params, options); + }) + .then(function () { + return _this.ejectAll(resourceName, params); + }); + deferred.resolve(); + } catch (err) { + deferred.reject(err); + } + + return promise; } module.exports = destroyAll; @@ -2143,65 +2135,67 @@ var errorPrefix = 'DS.find(resourceName, id[, options]): '; * ## Rejects with: * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` */ function find(resourceName, id, options) { - var deferred = this.$q.defer(), - promise = deferred.promise; - - options = options || {}; - - if (!this.definitions[resourceName]) { - deferred.reject(new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!')); - } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { - deferred.reject(new this.errors.IllegalArgumentError(errorPrefix + 'id: Must be a string or a number!', { id: { actual: typeof id, expected: 'string|number' } })); - } else if (!this.utils.isObject(options)) { - deferred.reject(new this.errors.IllegalArgumentError(errorPrefix + 'options: Must be an object!', { options: { actual: typeof options, expected: 'object' } })); - } else { - if (!('cacheResponse' in options)) { - options.cacheResponse = true; - } else { - options.cacheResponse = !!options.cacheResponse; - } - try { - var definition = this.definitions[resourceName], - resource = this.store[resourceName], - _this = this; + var deferred = this.$q.defer(); + var promise = deferred.promise; - if (options.bypassCache) { - delete resource.completedQueries[id]; - } + try { + var IA = this.errors.IA; - if (!(id in resource.completedQueries)) { - if (!(id in resource.pendingQueries)) { - promise = resource.pendingQueries[id] = _this.adapters[options.adapter || definition.defaultAdapter].find(definition, id, options) - .then(function (res) { - var data = definition.deserialize(resourceName, res); - if (options.cacheResponse) { - // Query is no longer pending - delete resource.pendingQueries[id]; - resource.completedQueries[id] = new Date().getTime(); - return _this.inject(resourceName, data); - } else { - return data; - } - }, function (err) { - delete resource.pendingQueries[id]; - return _this.$q.reject(err); - }); - } + options = options || {}; - return resource.pendingQueries[id]; - } else { - deferred.resolve(_this.get(resourceName, id)); - } - } catch (err) { - deferred.reject(err); - } - } + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { + throw new IA(errorPrefix + 'id: Must be a string or a number!'); + } else if (!this.utils.isObject(options)) { + throw new IA(errorPrefix + 'options: Must be an object!'); + } + + if (!('cacheResponse' in options)) { + options.cacheResponse = true; + } else { + options.cacheResponse = !!options.cacheResponse; + } + + var definition = this.definitions[resourceName]; + var resource = this.store[resourceName]; + var _this = this; + + if (options.bypassCache) { + delete resource.completedQueries[id]; + } + + if (!(id in resource.completedQueries)) { + if (!(id in resource.pendingQueries)) { + promise = resource.pendingQueries[id] = _this.adapters[options.adapter || definition.defaultAdapter].find(definition, id, options) + .then(function (res) { + var data = definition.deserialize(resourceName, res); + if (options.cacheResponse) { + // Query is no longer pending + delete resource.pendingQueries[id]; + resource.completedQueries[id] = new Date().getTime(); + return _this.inject(resourceName, data); + } else { + return data; + } + }, function (err) { + delete resource.pendingQueries[id]; + return _this.$q.reject(err); + }); + } + + return resource.pendingQueries[id]; + } else { + deferred.resolve(_this.get(resourceName, id)); + } + } catch (err) { + deferred.reject(err); + } - return promise; + return promise; } module.exports = find; @@ -2210,59 +2204,59 @@ module.exports = find; var errorPrefix = 'DS.findAll(resourceName, params[, options]): '; function processResults(utils, data, resourceName, queryHash) { - var resource = this.store[resourceName]; + var resource = this.store[resourceName]; - data = data || []; + data = data || []; - // Query is no longer pending - delete resource.pendingQueries[queryHash]; - resource.completedQueries[queryHash] = new Date().getTime(); + // Query is no longer pending + delete resource.pendingQueries[queryHash]; + resource.completedQueries[queryHash] = new Date().getTime(); - // Update modified timestamp of collection - resource.collectionModified = utils.updateTimestamp(resource.collectionModified); + // Update modified timestamp of collection + resource.collectionModified = utils.updateTimestamp(resource.collectionModified); - // Merge the new values into the cache - return this.inject(resourceName, data); + // Merge the new values into the cache + return this.inject(resourceName, data); } function _findAll(utils, resourceName, params, options) { - var definition = this.definitions[resourceName], - resource = this.store[resourceName], - _this = this, - queryHash = utils.toJson(params); - - if (options.bypassCache) { - delete resource.completedQueries[queryHash]; - } - - if (!(queryHash in resource.completedQueries)) { - // This particular query has never been completed - - if (!(queryHash in resource.pendingQueries)) { - - // This particular query has never even been made - resource.pendingQueries[queryHash] = _this.adapters[options.adapter || definition.defaultAdapter].findAll(definition, params, options) - .then(function (res) { - var data = definition.deserialize(resourceName, res); - if (options.cacheResponse) { - try { - return processResults.apply(_this, [utils, data, resourceName, queryHash]); - } catch (err) { - return _this.$q.reject(_this.errors.UnhandledError(err)); - } - } else { - return data; - } - }, function (err) { - delete resource.pendingQueries[queryHash]; - return _this.$q.reject(err); - }); - } + var definition = this.definitions[resourceName], + resource = this.store[resourceName], + _this = this, + queryHash = utils.toJson(params); + + if (options.bypassCache) { + delete resource.completedQueries[queryHash]; + } + + if (!(queryHash in resource.completedQueries)) { + // This particular query has never been completed + + if (!(queryHash in resource.pendingQueries)) { + + // This particular query has never even been made + resource.pendingQueries[queryHash] = _this.adapters[options.adapter || definition.defaultAdapter].findAll(definition, params, options) + .then(function (res) { + var data = definition.deserialize(resourceName, res); + if (options.cacheResponse) { + try { + return processResults.apply(_this, [utils, data, resourceName, queryHash]); + } catch (err) { + return _this.$q.reject(err); + } + } else { + return data; + } + }, function (err) { + delete resource.pendingQueries[queryHash]; + return _this.$q.reject(err); + }); + } - return resource.pendingQueries[queryHash]; - } else { - return this.filter(resourceName, params, options); - } + return resource.pendingQueries[queryHash]; + } else { + return this.filter(resourceName, params, options); + } } /** @@ -2281,7 +2275,7 @@ function _findAll(utils, resourceName, params, options) { * ## Example: * * ```js - * var query = { + * var params = { * where: { * author: { * '==': 'John Anderson' @@ -2289,17 +2283,13 @@ function _findAll(utils, resourceName, params, options) { * } * }; * - * DS.findAll('document', { - * query: query - * }).then(function (documents) { - * documents; // [{ id: 'aab7ff66-e21e-46e2-8be8-264d82aee535', author: 'John Anderson', title: 'How to cook' }, - * // { id: 'ee7f3f4d-98d5-4934-9e5a-6a559b08479f', author: 'John Anderson', title: 'How NOT to cook' }] + * DS.findAll('document', params).then(function (documents) { + * documents; // [{ id: '1', author: 'John Anderson', title: 'How to cook' }, + * // { id: '2', author: 'John Anderson', title: 'How NOT to cook' }] * * // The documents are now in the data store - * DS.filter('document', { - * query: query - * }); // [{ id: 'aab7ff66-e21e-46e2-8be8-264d82aee535', author: 'John Anderson', title: 'How to cook' }, - * // { id: 'ee7f3f4d-98d5-4934-9e5a-6a559b08479f', author: 'John Anderson', title: 'How NOT to cook' }] + * DS.filter('document', params); // [{ id: '1', author: 'John Anderson', title: 'How to cook' }, + * // { id: '2', author: 'John Anderson', title: 'How NOT to cook' }] * * }, function (err) { * // handle error @@ -2309,11 +2299,11 @@ function _findAll(utils, resourceName, params, options) { * @param {string} resourceName The resource type, e.g. 'user', 'comment', etc. * @param {object=} params Parameter object that is serialized into the query string. Properties: * - * - `{object=}` - `query` - The query object by which to filter items of the type specified by `resourceName`. Properties: - * - `{object=}` - `where` - Where clause. - * - `{number=}` - `limit` - Limit clause. - * - `{skip=}` - `skip` - Skip clause. - * - `{orderBy=}` - `orderBy` - OrderBy clause. + * - `{object=}` - `where` - Where clause. + * - `{number=}` - `limit` - Limit clause. + * - `{number=}` - `skip` - Skip clause. + * - `{number=}` - `offset` - Same as skip. + * - `{string|array=}` - `orderBy` - OrderBy clause. * * @param {object=} options Optional configuration. Properties: * - `{boolean=}` - `bypassCache` - Bypass the cache. Default: `false`. @@ -2328,138 +2318,286 @@ function _findAll(utils, resourceName, params, options) { * ## Rejects with: * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` */ function findAll(resourceName, params, options) { - var deferred = this.$q.defer(), - promise = deferred.promise, - _this = this; - - options = options || {}; - params = params || {}; - - if (!this.definitions[resourceName]) { - deferred.reject(new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!')); - } else if (!this.utils.isObject(params)) { - deferred.reject(new this.errors.IllegalArgumentError(errorPrefix + 'params: Must be an object!')); - } else if (!this.utils.isObject(options)) { - deferred.reject(new this.errors.IllegalArgumentError(errorPrefix + 'options: Must be an object!')); - } else { - if (!('cacheResponse' in options)) { - options.cacheResponse = true; - } else { - options.cacheResponse = !!options.cacheResponse; - } - try { - promise = promise.then(function () { - return _findAll.apply(_this, [_this.utils, resourceName, params, options]); - }); - deferred.resolve(); - } catch (err) { - deferred.reject(new this.errors.UnhandledError(err)); - } - } + var deferred = this.$q.defer(); + var promise = deferred.promise; + + try { + var IA = this.errors.IA; + var _this = this; + + options = options || {}; + params = params || {}; + + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isObject(params)) { + throw new IA(errorPrefix + 'params: Must be an object!'); + } else if (!this.utils.isObject(options)) { + throw new IA(errorPrefix + 'options: Must be an object!'); + } + + if (!('cacheResponse' in options)) { + options.cacheResponse = true; + } else { + options.cacheResponse = !!options.cacheResponse; + } + + promise = promise.then(function () { + return _findAll.apply(_this, [_this.utils, resourceName, params, options]); + }); + deferred.resolve(); + } catch (err) { + deferred.reject(err); + } - return promise; + return promise; } module.exports = findAll; },{}],37:[function(require,module,exports){ module.exports = { - /** - * @doc method - * @id DS.async_methods:create - * @name create - * @methodOf DS - * @description - * See [DS.create](/documentation/api/api/DS.async_methods:create). - */ - create: require('./create'), - - /** - * @doc method - * @id DS.async_methods:destroy - * @name destroy - * @methodOf DS - * @description - * See [DS.destroy](/documentation/api/api/DS.async_methods:destroy). - */ - destroy: require('./destroy'), - - /** - * @doc method - * @id DS.async_methods:destroyAll - * @name destroyAll - * @methodOf DS - * @description - * See [DS.destroyAll](/documentation/api/api/DS.async_methods:destroyAll). - */ - destroyAll: require('./destroyAll'), - - /** - * @doc method - * @id DS.async_methods:find - * @name find - * @methodOf DS - * @description - * See [DS.find](/documentation/api/api/DS.async_methods:find). - */ - find: require('./find'), - - /** - * @doc method - * @id DS.async_methods:findAll - * @name findAll - * @methodOf DS - * @description - * See [DS.findAll](/documentation/api/api/DS.async_methods:findAll). - */ - findAll: require('./findAll'), - - /** - * @doc method - * @id DS.async_methods:refresh - * @name refresh - * @methodOf DS - * @description - * See [DS.refresh](/documentation/api/api/DS.async_methods:refresh). - */ - refresh: require('./refresh'), - - /** - * @doc method - * @id DS.async_methods:save - * @name save - * @methodOf DS - * @description - * See [DS.save](/documentation/api/api/DS.async_methods:save). - */ - save: require('./save'), - - /** - * @doc method - * @id DS.async_methods:update - * @name update - * @methodOf DS - * @description - * See [DS.update](/documentation/api/api/DS.async_methods:update). - */ - update: require('./update'), - - /** - * @doc method - * @id DS.async_methods:updateAll - * @name updateAll - * @methodOf DS - * @description - * See [DS.updateAll](/documentation/api/api/DS.async_methods:updateAll). - */ - updateAll: require('./updateAll') + /** + * @doc method + * @id DS.async_methods:create + * @name create + * @methodOf DS + * @description + * See [DS.create](/documentation/api/api/DS.async_methods:create). + */ + create: require('./create'), + + /** + * @doc method + * @id DS.async_methods:destroy + * @name destroy + * @methodOf DS + * @description + * See [DS.destroy](/documentation/api/api/DS.async_methods:destroy). + */ + destroy: require('./destroy'), + + /** + * @doc method + * @id DS.async_methods:destroyAll + * @name destroyAll + * @methodOf DS + * @description + * See [DS.destroyAll](/documentation/api/api/DS.async_methods:destroyAll). + */ + destroyAll: require('./destroyAll'), + + /** + * @doc method + * @id DS.async_methods:find + * @name find + * @methodOf DS + * @description + * See [DS.find](/documentation/api/api/DS.async_methods:find). + */ + find: require('./find'), + + /** + * @doc method + * @id DS.async_methods:findAll + * @name findAll + * @methodOf DS + * @description + * See [DS.findAll](/documentation/api/api/DS.async_methods:findAll). + */ + findAll: require('./findAll'), + + /** + * @doc method + * @id DS.async_methods:loadRelations + * @name loadRelations + * @methodOf DS + * @description + * See [DS.loadRelations](/documentation/api/api/DS.async_methods:loadRelations). + */ + loadRelations: require('./loadRelations'), + + /** + * @doc method + * @id DS.async_methods:refresh + * @name refresh + * @methodOf DS + * @description + * See [DS.refresh](/documentation/api/api/DS.async_methods:refresh). + */ + refresh: require('./refresh'), + + /** + * @doc method + * @id DS.async_methods:save + * @name save + * @methodOf DS + * @description + * See [DS.save](/documentation/api/api/DS.async_methods:save). + */ + save: require('./save'), + + /** + * @doc method + * @id DS.async_methods:update + * @name update + * @methodOf DS + * @description + * See [DS.update](/documentation/api/api/DS.async_methods:update). + */ + update: require('./update'), + + /** + * @doc method + * @id DS.async_methods:updateAll + * @name updateAll + * @methodOf DS + * @description + * See [DS.updateAll](/documentation/api/api/DS.async_methods:updateAll). + */ + updateAll: require('./updateAll') }; -},{"./create":32,"./destroy":33,"./destroyAll":34,"./find":35,"./findAll":36,"./refresh":38,"./save":39,"./update":40,"./updateAll":41}],38:[function(require,module,exports){ +},{"./create":32,"./destroy":33,"./destroyAll":34,"./find":35,"./findAll":36,"./loadRelations":38,"./refresh":39,"./save":40,"./update":41,"./updateAll":42}],38:[function(require,module,exports){ +var errorPrefix = 'DS.loadRelations(resourceName, instance(Id), relations[, options]): '; + +/** + * @doc method + * @id DS.async_methods:loadRelations + * @name loadRelations + * @description + * Asynchronously load the indicated relations of the given instance. + * + * ## Signature: + * ```js + * DS.loadRelations(resourceName, instance(Id), relations[, options]) + * ``` + * + * ## Examples: + * + * ```js + * DS.loadRelations('user', 10, ['profile']).then(function (user) { + * user.profile; // object + * assert.deepEqual(user.profile, DS.filter('profile', { userId: 10 })[0]); + * }); + * ``` + * + * ```js + * var user = DS.get('user', 10); + * + * DS.loadRelations('user', user, ['profile']).then(function (user) { + * user.profile; // object + * assert.deepEqual(user.profile, DS.filter('profile', { userId: 10 })[0]); + * }); + * ``` + * + * ```js + * DS.loadRelations('user', 10, ['profile'], { cacheResponse: false }).then(function (user) { + * user.profile; // object + * assert.equal(DS.filter('profile', { userId: 10 }).length, 0); + * }); + * ``` + * + * @param {string} resourceName The resource type, e.g. 'user', 'comment', etc. + * @param {string|number|object} instance The instance or the id of the instance for which relations are to be loaded. + * @param {string|array=} relations The relation(s) to load. + * @param {object=} options Optional configuration that is passed to the `find` and `findAll` methods that may be called. + * + * @returns {Promise} Promise produced by the `$q` service. + * + * ## Resolves with: + * + * - `{object}` - `item` - The instance with its loaded relations. + * + * ## Rejects with: + * + * - `{IllegalArgumentError}` + * - `{NonexistentResourceError}` + */ +function loadRelations(resourceName, instance, relations, options) { + var deferred = this.$q.defer(); + var promise = deferred.promise; + + try { + var IA = this.errors.IA; + + options = options || {}; + + if (angular.isString(instance) || angular.isNumber(instance)) { + instance = this.get(resourceName, instance); + } + + if (angular.isString(relations)) { + relations = [relations]; + } + + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isObject(instance)) { + throw new IA(errorPrefix + 'instance(Id): Must be a string, number or object!'); + } else if (!this.utils.isArray(relations)) { + throw new IA(errorPrefix + 'relations: Must be a string or an array!'); + } else if (!this.utils.isObject(options)) { + throw new IA(errorPrefix + 'options: Must be an object!'); + } + + var definition = this.definitions[resourceName]; + var _this = this; + var tasks = []; + var fields = []; + + _this.utils.forOwn(definition.relations, function (relation, type) { + _this.utils.forOwn(relation, function (def, relationName) { + if (_this.utils.contains(relations, relationName)) { + var task; + var params = {}; + params[def.foreignKey] = instance[definition.idAttribute]; + + if (type === 'hasMany') { + task = _this.findAll(relationName, params, options); + } else if (type === 'hasOne') { + if (def.localKey && instance[def.localKey]) { + task = _this.find(relationName, instance[def.localKey], options); + } else if (def.foreignKey) { + task = _this.findAll(relationName, params, options); + } + } else { + task = _this.find(relationName, instance[def.localKey], options); + } + + if (task) { + tasks.push(task); + fields.push(def.localField); + } + } + }); + }); + + promise = promise + .then(function () { + return _this.$q.all(tasks); + }) + .then(function (loadedRelations) { + angular.forEach(fields, function (field, index) { + instance[field] = loadedRelations[index]; + }); + return instance; + }); + + deferred.resolve(); + } catch (err) { + deferred.reject(err); + } + + return promise; +} + +module.exports = loadRelations; + +},{}],39:[function(require,module,exports){ var errorPrefix = 'DS.refresh(resourceName, id[, options]): '; /** @@ -2493,7 +2631,7 @@ var errorPrefix = 'DS.refresh(resourceName, id[, options]): '; * ## Throws * * - `{IllegalArgumentError}` - * - `{RuntimeError}` + * - `{NonexistentResourceError}` * * @param {string} resourceName The resource type, e.g. 'user', 'comment', etc. * @param {string|number} id The primary key of the item to refresh from the server. @@ -2508,32 +2646,33 @@ var errorPrefix = 'DS.refresh(resourceName, id[, options]): '; * ## Rejects with: * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` */ function refresh(resourceName, id, options) { - options = options || {}; - - if (!this.definitions[resourceName]) { - throw new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!'); - } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'id: Must be a string or a number!', { id: { actual: typeof id, expected: 'string|number' } }); - } else if (!this.utils.isObject(options)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'options: Must be an object!', { options: { actual: typeof options, expected: 'object' } }); - } else { - options.bypassCache = true; - - if (this.get(resourceName, id)) { - return this.find(resourceName, id, options); - } else { - return false; - } - } + var IA = this.errors.IA; + + options = options || {}; + + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { + throw new IA(errorPrefix + 'id: Must be a string or a number!'); + } else if (!this.utils.isObject(options)) { + throw new IA(errorPrefix + 'options: Must be an object!'); + } else { + options.bypassCache = true; + + if (this.get(resourceName, id)) { + return this.find(resourceName, id, options); + } else { + return false; + } + } } module.exports = refresh; -},{}],39:[function(require,module,exports){ +},{}],40:[function(require,module,exports){ var errorPrefix = 'DS.save(resourceName, id[, options]): '; /** @@ -2576,83 +2715,90 @@ var errorPrefix = 'DS.save(resourceName, id[, options]): '; * * - `{IllegalArgumentError}` * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` */ function save(resourceName, id, options) { - var deferred = this.$q.defer(), - promise = deferred.promise; - - options = options || {}; - - if (!this.definitions[resourceName]) { - deferred.reject(new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!')); - } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { - deferred.reject(new this.errors.IllegalArgumentError(errorPrefix + 'id: Must be a string or a number!', { id: { actual: typeof id, expected: 'string|number' } })); - } else if (!this.utils.isObject(options)) { - deferred.reject(new this.errors.IllegalArgumentError(errorPrefix + 'options: Must be an object!', { options: { actual: typeof options, expected: 'object' } })); - } else { - var item = this.get(resourceName, id); - if (!item) { - deferred.reject(new this.errors.RuntimeError(errorPrefix + 'id: "' + id + '" not found!')); - } else { - var definition = this.definitions[resourceName], - resource = this.store[resourceName], - _this = this; - - promise = promise - .then(function (attrs) { - return _this.$q.promisify(definition.beforeValidate)(resourceName, attrs); - }) - .then(function (attrs) { - return _this.$q.promisify(definition.validate)(resourceName, attrs); - }) - .then(function (attrs) { - return _this.$q.promisify(definition.afterValidate)(resourceName, attrs); - }) - .then(function (attrs) { - return _this.$q.promisify(definition.beforeUpdate)(resourceName, attrs); - }) - .then(function (attrs) { - if (options.changesOnly) { - resource.observers[id].deliver(); - var toKeep = [], - changes = _this.changes(resourceName, id); - - for (var key in changes.added) { - toKeep.push(key); - } - for (key in changes.changed) { - toKeep.push(key); - } - changes = _this.utils.pick(attrs, toKeep); - if (_this.utils.isEmpty(changes)) { - // no changes, return - return attrs; - } else { - attrs = changes; - } - } - return _this.adapters[options.adapter || definition.defaultAdapter].update(definition, id, definition.serialize(resourceName, attrs), options); - }) - .then(function (res) { - return _this.$q.promisify(definition.afterUpdate)(resourceName, definition.deserialize(resourceName, res)); - }) - .then(function (data) { - _this.inject(definition.name, data, options); - resource.previousAttributes[id] = _this.utils.deepMixIn({}, data); - resource.saved[id] = _this.utils.updateTimestamp(resource.saved[id]); - return _this.get(resourceName, id); - }); - - deferred.resolve(item); - } - } - return promise; + var deferred = this.$q.defer(); + var promise = deferred.promise; + + try { + var IA = this.errors.IA; + + options = options || {}; + + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { + throw new IA(errorPrefix + 'id: Must be a string or a number!'); + } else if (!this.utils.isObject(options)) { + throw new IA(errorPrefix + 'options: Must be an object!'); + } + + var item = this.get(resourceName, id); + if (!item) { + throw new this.errors.R(errorPrefix + 'id: "' + id + '" not found!'); + } + + var definition = this.definitions[resourceName]; + var resource = this.store[resourceName]; + var _this = this; + + promise = promise + .then(function (attrs) { + return _this.$q.promisify(definition.beforeValidate)(resourceName, attrs); + }) + .then(function (attrs) { + return _this.$q.promisify(definition.validate)(resourceName, attrs); + }) + .then(function (attrs) { + return _this.$q.promisify(definition.afterValidate)(resourceName, attrs); + }) + .then(function (attrs) { + return _this.$q.promisify(definition.beforeUpdate)(resourceName, attrs); + }) + .then(function (attrs) { + if (options.changesOnly) { + resource.observers[id].deliver(); + var toKeep = [], + changes = _this.changes(resourceName, id); + + for (var key in changes.added) { + toKeep.push(key); + } + for (key in changes.changed) { + toKeep.push(key); + } + changes = _this.utils.pick(attrs, toKeep); + if (_this.utils.isEmpty(changes)) { + // no changes, return + return attrs; + } else { + attrs = changes; + } + } + return _this.adapters[options.adapter || definition.defaultAdapter].update(definition, id, definition.serialize(resourceName, attrs), options); + }) + .then(function (res) { + return _this.$q.promisify(definition.afterUpdate)(resourceName, definition.deserialize(resourceName, res)); + }) + .then(function (data) { + _this.inject(definition.name, data, options); + resource.previousAttributes[id] = _this.utils.deepMixIn({}, data); + resource.saved[id] = _this.utils.updateTimestamp(resource.saved[id]); + return _this.get(resourceName, id); + }); + + deferred.resolve(item); + } catch (err) { + deferred.reject(err); + } + + return promise; } module.exports = save; -},{}],40:[function(require,module,exports){ +},{}],41:[function(require,module,exports){ var errorPrefix = 'DS.update(resourceName, id, attrs[, options]): '; /** @@ -2697,73 +2843,79 @@ var errorPrefix = 'DS.update(resourceName, id, attrs[, options]): '; * ## Rejects with: * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` */ function update(resourceName, id, attrs, options) { - var deferred = this.$q.defer(), - promise = deferred.promise; - - options = options || {}; - - if (!this.definitions[resourceName]) { - deferred.reject(new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!')); - } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { - deferred.reject(new this.errors.IllegalArgumentError(errorPrefix + 'id: Must be a string or a number!')); - } else if (!this.utils.isObject(attrs)) { - deferred.reject(new this.errors.IllegalArgumentError(errorPrefix + 'attrs: Must be an object!')); - } else if (!this.utils.isObject(options)) { - deferred.reject(new this.errors.IllegalArgumentError(errorPrefix + 'options: Must be an object!')); - } else { - var definition = this.definitions[resourceName], - resource = this.store[resourceName], - _this = this; - - if (!('cacheResponse' in options)) { - options.cacheResponse = true; - } else { - options.cacheResponse = !!options.cacheResponse; - } + var deferred = this.$q.defer(); + var promise = deferred.promise; + + try { + var IA = this.errors.IA; + + options = options || {}; + + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { + throw new IA(errorPrefix + 'id: Must be a string or a number!'); + } else if (!this.utils.isObject(attrs)) { + throw new IA(errorPrefix + 'attrs: Must be an object!'); + } else if (!this.utils.isObject(options)) { + throw new IA(errorPrefix + 'options: Must be an object!'); + } - promise = promise - .then(function (attrs) { - return _this.$q.promisify(definition.beforeValidate)(resourceName, attrs); - }) - .then(function (attrs) { - return _this.$q.promisify(definition.validate)(resourceName, attrs); - }) - .then(function (attrs) { - return _this.$q.promisify(definition.afterValidate)(resourceName, attrs); - }) - .then(function (attrs) { - return _this.$q.promisify(definition.beforeUpdate)(resourceName, attrs); - }) - .then(function (attrs) { - return _this.adapters[options.adapter || definition.defaultAdapter].update(definition, id, definition.serialize(resourceName, attrs), options); - }) - .then(function (res) { - return _this.$q.promisify(definition.afterUpdate)(resourceName, definition.deserialize(resourceName, res)); - }) - .then(function (data) { - if (options.cacheResponse) { - var updated = _this.inject(definition.name, data, options); - var id = updated[definition.idAttribute]; - resource.previousAttributes[id] = _this.utils.deepMixIn({}, updated); - resource.saved[id] = _this.utils.updateTimestamp(resource.saved[id]); - return _this.get(definition.name, id); - } else { - return data; - } - }); + var definition = this.definitions[resourceName]; + var resource = this.store[resourceName]; + var _this = this; - deferred.resolve(attrs); - } - return promise; + if (!('cacheResponse' in options)) { + options.cacheResponse = true; + } else { + options.cacheResponse = !!options.cacheResponse; + } + + promise = promise + .then(function (attrs) { + return _this.$q.promisify(definition.beforeValidate)(resourceName, attrs); + }) + .then(function (attrs) { + return _this.$q.promisify(definition.validate)(resourceName, attrs); + }) + .then(function (attrs) { + return _this.$q.promisify(definition.afterValidate)(resourceName, attrs); + }) + .then(function (attrs) { + return _this.$q.promisify(definition.beforeUpdate)(resourceName, attrs); + }) + .then(function (attrs) { + return _this.adapters[options.adapter || definition.defaultAdapter].update(definition, id, definition.serialize(resourceName, attrs), options); + }) + .then(function (res) { + return _this.$q.promisify(definition.afterUpdate)(resourceName, definition.deserialize(resourceName, res)); + }) + .then(function (data) { + if (options.cacheResponse) { + var updated = _this.inject(definition.name, data, options); + var id = updated[definition.idAttribute]; + resource.previousAttributes[id] = _this.utils.deepMixIn({}, updated); + resource.saved[id] = _this.utils.updateTimestamp(resource.saved[id]); + return _this.get(definition.name, id); + } else { + return data; + } + }); + + deferred.resolve(attrs); + } catch (err) { + deferred.reject(err); + } + + return promise; } module.exports = update; -},{}],41:[function(require,module,exports){ +},{}],42:[function(require,module,exports){ var errorPrefix = 'DS.updateAll(resourceName, attrs, params[, options]): '; /** @@ -2786,11 +2938,9 @@ var errorPrefix = 'DS.updateAll(resourceName, attrs, params[, options]): '; * DS.filter('document'); // [] * * DS.updateAll('document', 5, { author: 'Sally' }, { - * query: { - * where: { - * author: { - * '==': 'John Anderson' - * } + * where: { + * author: { + * '==': 'John Anderson' * } * } * }) @@ -2806,11 +2956,11 @@ var errorPrefix = 'DS.updateAll(resourceName, attrs, params[, options]): '; * @param {object} attrs The attributes with which to update the items. * @param {object} params Parameter object that is serialized into the query string. Properties: * - * - `{object=}` - `query` - The query object by which to filter items of the type specified by `resourceName`. Properties: - * - `{object=}` - `where` - Where clause. - * - `{number=}` - `limit` - Limit clause. - * - `{skip=}` - `skip` - Skip clause. - * - `{orderBy=}` - `orderBy` - OrderBy clause. + * - `{object=}` - `where` - Where clause. + * - `{number=}` - `limit` - Limit clause. + * - `{number=}` - `skip` - Skip clause. + * - `{number=}` - `offset` - Same as skip. + * - `{string|array=}` - `orderBy` - OrderBy clause. * * @param {object=} options Optional configuration. Properties: * - `{boolean=}` - `cacheResponse` - Inject the data returned by the server into the data store. Default: `true`. @@ -2824,114 +2974,264 @@ var errorPrefix = 'DS.updateAll(resourceName, attrs, params[, options]): '; * ## Rejects with: * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` */ function updateAll(resourceName, attrs, params, options) { - var deferred = this.$q.defer(), - promise = deferred.promise; - - options = options || {}; - - if (!this.definitions[resourceName]) { - deferred.reject(new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!')); - } else if (!this.utils.isObject(attrs)) { - deferred.reject(new this.errors.IllegalArgumentError(errorPrefix + 'attrs: Must be an object!')); - } else if (!this.utils.isObject(params)) { - deferred.reject(new this.errors.IllegalArgumentError(errorPrefix + 'params: Must be an object!')); - } else if (!this.utils.isObject(options)) { - deferred.reject(new this.errors.IllegalArgumentError(errorPrefix + 'options: Must be an object!')); - } else { - var definition = this.definitions[resourceName], - resource = this.store[resourceName], - _this = this; - - if (!('cacheResponse' in options)) { - options.cacheResponse = true; - } else { - options.cacheResponse = !!options.cacheResponse; - } + var deferred = this.$q.defer(); + var promise = deferred.promise; + + try { + var IA = this.errors.IA; + + options = options || {}; + + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isObject(attrs)) { + throw new IA(errorPrefix + 'attrs: Must be an object!'); + } else if (!this.utils.isObject(params)) { + throw new IA(errorPrefix + 'params: Must be an object!'); + } else if (!this.utils.isObject(options)) { + throw new IA(errorPrefix + 'options: Must be an object!'); + } - promise = promise - .then(function (attrs) { - return _this.$q.promisify(definition.beforeValidate)(resourceName, attrs); - }) - .then(function (attrs) { - return _this.$q.promisify(definition.validate)(resourceName, attrs); - }) - .then(function (attrs) { - return _this.$q.promisify(definition.afterValidate)(resourceName, attrs); - }) - .then(function (attrs) { - return _this.$q.promisify(definition.beforeUpdate)(resourceName, attrs); - }) - .then(function (attrs) { - return _this.adapters[options.adapter || definition.defaultAdapter].updateAll(definition, definition.serialize(resourceName, attrs), params, options); - }) - .then(function (res) { - return _this.$q.promisify(definition.afterUpdate)(resourceName, definition.deserialize(resourceName, res)); - }) - .then(function (data) { - if (options.cacheResponse) { - return _this.inject(definition.name, data, options); - } else { - return data; - } - }); + var definition = this.definitions[resourceName]; + var _this = this; - deferred.resolve(attrs); - } - return promise; + if (!('cacheResponse' in options)) { + options.cacheResponse = true; + } else { + options.cacheResponse = !!options.cacheResponse; + } + + promise = promise + .then(function (attrs) { + return _this.$q.promisify(definition.beforeValidate)(resourceName, attrs); + }) + .then(function (attrs) { + return _this.$q.promisify(definition.validate)(resourceName, attrs); + }) + .then(function (attrs) { + return _this.$q.promisify(definition.afterValidate)(resourceName, attrs); + }) + .then(function (attrs) { + return _this.$q.promisify(definition.beforeUpdate)(resourceName, attrs); + }) + .then(function (attrs) { + return _this.adapters[options.adapter || definition.defaultAdapter].updateAll(definition, definition.serialize(resourceName, attrs), params, options); + }) + .then(function (res) { + return _this.$q.promisify(definition.afterUpdate)(resourceName, definition.deserialize(resourceName, res)); + }) + .then(function (data) { + if (options.cacheResponse) { + return _this.inject(definition.name, data, options); + } else { + return data; + } + }); + + deferred.resolve(attrs); + } catch (err) { + deferred.reject(err); + } + + return promise; } module.exports = updateAll; -},{}],42:[function(require,module,exports){ +},{}],43:[function(require,module,exports){ var utils = require('../utils')[0](); function lifecycleNoop(resourceName, attrs, cb) { - cb(null, attrs); + cb(null, attrs); } -function BaseConfig() { +function Defaults() { } -BaseConfig.prototype.idAttribute = 'id'; -BaseConfig.prototype.defaultAdapter = 'DSHttpAdapter'; -BaseConfig.prototype.filter = function (resourceName, where, attrs) { - var keep = true; - utils.forOwn(where, function (clause, field) { - if (utils.isString(clause)) { - clause = { - '===': clause - }; - } else if (utils.isNumber(clause)) { - clause = { - '==': clause - }; - } - if ('==' in clause) { - keep = keep && (attrs[field] == clause['==']); - } else if ('===' in clause) { - keep = keep && (attrs[field] === clause['===']); - } else if ('!=' in clause) { - keep = keep && (attrs[field] != clause['!=']); - } else if ('>' in clause) { - keep = keep && (attrs[field] > clause['>']); - } else if ('>=' in clause) { - keep = keep && (attrs[field] >= clause['>=']); - } else if ('<' in clause) { - keep = keep && (attrs[field] < clause['<']); - } else if ('<=' in clause) { - keep = keep && (attrs[field] <= clause['<=']); - } else if ('in' in clause) { - keep = keep && utils.contains(clause['in'], attrs[field]); - } - }); - return keep; +Defaults.prototype.idAttribute = 'id'; +Defaults.prototype.defaultAdapter = 'DSHttpAdapter'; +Defaults.prototype.filter = function (collection, resourceName, params, options) { + var _this = this; + var filtered = collection; + var where = null; + var reserved = { + skip: '', + offset: '', + where: '', + limit: '', + orderBy: '', + sort: '' + }; + + if (this.utils.isObject(params.where)) { + where = params.where; + } else { + where = {}; + } + + if (options.allowSimpleWhere) { + this.utils.forOwn(params, function (value, key) { + if (!(key in reserved) && !(key in where)) { + where[key] = { + '==': value + }; + } + }); + } + + if (this.utils.isEmpty(where)) { + where = null; + } + + if (where) { + filtered = this.utils.filter(filtered, function (attrs) { +// console.log(attrs); + var first = true; + var keep = true; + _this.utils.forOwn(where, function (clause, field) { +// console.log(clause, field); + if (_this.utils.isString(clause)) { + clause = { + '===': clause + }; + } else if (_this.utils.isNumber(clause)) { + clause = { + '==': clause + }; + } + if (_this.utils.isObject(clause)) { + _this.utils.forOwn(clause, function (val, op) { +// console.log(op, val); + if (op === '==') { + keep = first ? (attrs[field] == val) : keep && (attrs[field] == val); + } else if (op === '===') { + keep = first ? (attrs[field] === val) : keep && (attrs[field] === val); + } else if (op === '!=') { + keep = first ? (attrs[field] != val) : keep && (attrs[field] != val); + } else if (op === '!==') { + keep = first ? (attrs[field] !== val) : keep && (attrs[field] !== val); + } else if (op === '>') { + keep = first ? (attrs[field] > val) : keep && (attrs[field] > val); + } else if (op === '>=') { + keep = first ? (attrs[field] >= val) : keep && (attrs[field] >= val); + } else if (op === '<') { + keep = first ? (attrs[field] < val) : keep && (attrs[field] < val); + } else if (op === '<=') { + keep = first ? (attrs[field] <= val) : keep && (attrs[field] <= val); + } else if (op === 'in') { + keep = first ? _this.utils.contains(val, attrs[field]) : keep && _this.utils.contains(val, attrs[field]); + } else if (op === '|==') { + keep = first ? (attrs[field] == val) : keep || (attrs[field] == val); + } else if (op === '|===') { + keep = first ? (attrs[field] === val) : keep || (attrs[field] === val); + } else if (op === '|!=') { + keep = first ? (attrs[field] != val) : keep || (attrs[field] != val); + } else if (op === '|!==') { + keep = first ? (attrs[field] !== val) : keep || (attrs[field] !== val); + } else if (op === '|>') { + keep = first ? (attrs[field] > val) : keep || (attrs[field] > val); + } else if (op === '|>=') { + keep = first ? (attrs[field] >= val) : keep || (attrs[field] >= val); + } else if (op === '|<') { + keep = first ? (attrs[field] < val) : keep || (attrs[field] < val); + } else if (op === '|<=') { + keep = first ? (attrs[field] <= val) : keep || (attrs[field] <= val); + } else if (op === '|in') { + keep = first ? _this.utils.contains(val, attrs[field]) : keep || _this.utils.contains(val, attrs[field]); + } + first = false; +// console.log(keep, first); + }); + } + }); + return keep; + }); + } + + var orderBy = null; + + if (this.utils.isString(params.orderBy)) { + orderBy = [ + [params.orderBy, 'ASC'] + ]; + } else if (this.utils.isArray(params.orderBy)) { + orderBy = params.orderBy; + } + + if (!orderBy && this.utils.isString(params.sort)) { + orderBy = [ + [params.sort, 'ASC'] + ]; + } else if (!orderBy && this.utils.isArray(params.sort)) { + orderBy = params.sort; + } + + // Apply 'orderBy' + if (orderBy) { + angular.forEach(orderBy, function (def) { + if (_this.utils.isString(def)) { + def = [def, 'ASC']; + } else if (!_this.utils.isArray(def)) { + throw new _this.errors.IllegalArgumentError('DS.filter(resourceName[, params][, options]): ' + angular.toJson(def) + ': Must be a string or an array!', { params: { 'orderBy[i]': { actual: typeof def, expected: 'string|array' } } }); + } + filtered = _this.utils.sort(filtered, function (a, b) { + var cA = a[def[0]], cB = b[def[0]]; + if (_this.utils.isString(cA)) { + cA = _this.utils.upperCase(cA); + } + if (_this.utils.isString(cB)) { + cB = _this.utils.upperCase(cB); + } + if (def[1] === 'DESC') { + if (cB < cA) { + return -1; + } else if (cB > cA) { + return 1; + } else { + return 0; + } + } else { + if (cA < cB) { + return -1; + } else if (cA > cB) { + return 1; + } else { + return 0; + } + } + }); + }); + } + + var limit = angular.isNumber(params.limit) ? params.limit : null; + var skip = null; + + if (angular.isNumber(params.skip)) { + skip = params.skip; + } else if (angular.isNumber(params.offset)) { + skip = params.offset; + } + + // Apply 'limit' and 'skip' + if (limit && skip) { + filtered = this.utils.slice(filtered, skip, Math.min(filtered.length, skip + limit)); + } else if (this.utils.isNumber(limit)) { + filtered = this.utils.slice(filtered, 0, Math.min(filtered.length, limit)); + } else if (this.utils.isNumber(skip)) { + if (skip < filtered.length) { + filtered = this.utils.slice(filtered, skip); + } else { + filtered = []; + } + } + + return filtered; }; -BaseConfig.prototype.baseUrl = ''; -BaseConfig.prototype.endpoint = ''; +Defaults.prototype.baseUrl = ''; +Defaults.prototype.endpoint = ''; /** * @doc property * @id DSProvider.properties:defaults.beforeValidate @@ -2962,7 +3262,7 @@ BaseConfig.prototype.endpoint = ''; * @param {string} resourceName The name of the resource moving through the lifecycle. * @param {object} attrs Attributes of the item moving through the lifecycle. */ -BaseConfig.prototype.beforeValidate = lifecycleNoop; +Defaults.prototype.beforeValidate = lifecycleNoop; /** * @doc property * @id DSProvider.properties:defaults.validate @@ -2993,7 +3293,7 @@ BaseConfig.prototype.beforeValidate = lifecycleNoop; * @param {string} resourceName The name of the resource moving through the lifecycle. * @param {object} attrs Attributes of the item moving through the lifecycle. */ -BaseConfig.prototype.validate = lifecycleNoop; +Defaults.prototype.validate = lifecycleNoop; /** * @doc property * @id DSProvider.properties:defaults.afterValidate @@ -3024,7 +3324,7 @@ BaseConfig.prototype.validate = lifecycleNoop; * @param {string} resourceName The name of the resource moving through the lifecycle. * @param {object} attrs Attributes of the item moving through the lifecycle. */ -BaseConfig.prototype.afterValidate = lifecycleNoop; +Defaults.prototype.afterValidate = lifecycleNoop; /** * @doc property * @id DSProvider.properties:defaults.beforeCreate @@ -3055,7 +3355,7 @@ BaseConfig.prototype.afterValidate = lifecycleNoop; * @param {string} resourceName The name of the resource moving through the lifecycle. * @param {object} attrs Attributes of the item moving through the lifecycle. */ -BaseConfig.prototype.beforeCreate = lifecycleNoop; +Defaults.prototype.beforeCreate = lifecycleNoop; /** * @doc property * @id DSProvider.properties:defaults.afterCreate @@ -3086,7 +3386,7 @@ BaseConfig.prototype.beforeCreate = lifecycleNoop; * @param {string} resourceName The name of the resource moving through the lifecycle. * @param {object} attrs Attributes of the item moving through the lifecycle. */ -BaseConfig.prototype.afterCreate = lifecycleNoop; +Defaults.prototype.afterCreate = lifecycleNoop; /** * @doc property * @id DSProvider.properties:defaults.beforeUpdate @@ -3117,7 +3417,7 @@ BaseConfig.prototype.afterCreate = lifecycleNoop; * @param {string} resourceName The name of the resource moving through the lifecycle. * @param {object} attrs Attributes of the item moving through the lifecycle. */ -BaseConfig.prototype.beforeUpdate = lifecycleNoop; +Defaults.prototype.beforeUpdate = lifecycleNoop; /** * @doc property * @id DSProvider.properties:defaults.afterUpdate @@ -3148,7 +3448,7 @@ BaseConfig.prototype.beforeUpdate = lifecycleNoop; * @param {string} resourceName The name of the resource moving through the lifecycle. * @param {object} attrs Attributes of the item moving through the lifecycle. */ -BaseConfig.prototype.afterUpdate = lifecycleNoop; +Defaults.prototype.afterUpdate = lifecycleNoop; /** * @doc property * @id DSProvider.properties:defaults.beforeDestroy @@ -3179,7 +3479,7 @@ BaseConfig.prototype.afterUpdate = lifecycleNoop; * @param {string} resourceName The name of the resource moving through the lifecycle. * @param {object} attrs Attributes of the item moving through the lifecycle. */ -BaseConfig.prototype.beforeDestroy = lifecycleNoop; +Defaults.prototype.beforeDestroy = lifecycleNoop; /** * @doc property * @id DSProvider.properties:defaults.afterDestroy @@ -3210,7 +3510,7 @@ BaseConfig.prototype.beforeDestroy = lifecycleNoop; * @param {string} resourceName The name of the resource moving through the lifecycle. * @param {object} attrs Attributes of the item moving through the lifecycle. */ -BaseConfig.prototype.afterDestroy = lifecycleNoop; +Defaults.prototype.afterDestroy = lifecycleNoop; /** * @doc property * @id DSProvider.properties:defaults.beforeInject @@ -3235,8 +3535,8 @@ BaseConfig.prototype.afterDestroy = lifecycleNoop; * @param {string} resourceName The name of the resource moving through the lifecycle. * @param {object} attrs Attributes of the item moving through the lifecycle. */ -BaseConfig.prototype.beforeInject = function (resourceName, attrs) { - return attrs; +Defaults.prototype.beforeInject = function (resourceName, attrs) { + return attrs; }; /** * @doc property @@ -3262,8 +3562,8 @@ BaseConfig.prototype.beforeInject = function (resourceName, attrs) { * @param {string} resourceName The name of the resource moving through the lifecycle. * @param {object} attrs Attributes of the item moving through the lifecycle. */ -BaseConfig.prototype.afterInject = function (resourceName, attrs) { - return attrs; +Defaults.prototype.afterInject = function (resourceName, attrs) { + return attrs; }; /** * @doc property @@ -3286,8 +3586,8 @@ BaseConfig.prototype.afterInject = function (resourceName, attrs) { * @param {object} data Data to be sent to the server. * @returns {*} By default returns `data` as-is. */ -BaseConfig.prototype.serialize = function (resourceName, data) { - return data; +Defaults.prototype.serialize = function (resourceName, data) { + return data; }; /** @@ -3309,8 +3609,8 @@ BaseConfig.prototype.serialize = function (resourceName, data) { * @param {object} data Response object from `$http()`. * @returns {*} By default returns `data.data`. */ -BaseConfig.prototype.deserialize = function (resourceName, data) { - return data.data; +Defaults.prototype.deserialize = function (resourceName, data) { + return data.data; }; /** @@ -3320,153 +3620,154 @@ BaseConfig.prototype.deserialize = function (resourceName, data) { */ function DSProvider() { - /** - * @doc property - * @id DSProvider.properties:defaults - * @name defaults - * @description - * See the [configuration guide](/documentation/guide/configure/global). - * - * Properties: - * - * - `{string}` - `baseUrl` - The url relative to which all AJAX requests will be made. - * - `{string}` - `idAttribute` - Default: `"id"` - The attribute that specifies the primary key for resources. - * - `{string}` - `defaultAdapter` - Default: `"DSHttpAdapter"` - * - `{function}` - `filter` - Default: See [angular-data query language](/documentation/guide/queries/custom). - * - `{function}` - `beforeValidate` - See [DSProvider.defaults.beforeValidate](/documentation/api/angular-data/DSProvider.properties:defaults.beforeValidate). Default: No-op - * - `{function}` - `validate` - See [DSProvider.defaults.validate](/documentation/api/angular-data/DSProvider.properties:defaults.validate). Default: No-op - * - `{function}` - `afterValidate` - See [DSProvider.defaults.afterValidate](/documentation/api/angular-data/DSProvider.properties:defaults.afterValidate). Default: No-op - * - `{function}` - `beforeCreate` - See [DSProvider.defaults.beforeCreate](/documentation/api/angular-data/DSProvider.properties:defaults.beforeCreate). Default: No-op - * - `{function}` - `afterCreate` - See [DSProvider.defaults.afterCreate](/documentation/api/angular-data/DSProvider.properties:defaults.afterCreate). Default: No-op - * - `{function}` - `beforeUpdate` - See [DSProvider.defaults.beforeUpdate](/documentation/api/angular-data/DSProvider.properties:defaults.beforeUpdate). Default: No-op - * - `{function}` - `afterUpdate` - See [DSProvider.defaults.afterUpdate](/documentation/api/angular-data/DSProvider.properties:defaults.afterUpdate). Default: No-op - * - `{function}` - `beforeDestroy` - See [DSProvider.defaults.beforeDestroy](/documentation/api/angular-data/DSProvider.properties:defaults.beforeDestroy). Default: No-op - * - `{function}` - `afterDestroy` - See [DSProvider.defaults.afterDestroy](/documentation/api/angular-data/DSProvider.properties:defaults.afterDestroy). Default: No-op - * - `{function}` - `afterInject` - See [DSProvider.defaults.afterInject](/documentation/api/angular-data/DSProvider.properties:defaults.afterInject). Default: No-op - * - `{function}` - `beforeInject` - See [DSProvider.defaults.beforeInject](/documentation/api/angular-data/DSProvider.properties:defaults.beforeInject). Default: No-op - * - `{function}` - `serialize` - See [DSProvider.defaults.serialize](/documentation/api/angular-data/DSProvider.properties:defaults.serialize). Default: No-op - * - `{function}` - `deserialize` - See [DSProvider.defaults.deserialize](/documentation/api/angular-data/DSProvider.properties:defaults.deserialize). Default: No-op - */ - var defaults = this.defaults = new BaseConfig(); - - this.$get = [ - '$rootScope', '$log', '$q', 'DSHttpAdapter', 'DSLocalStorageAdapter', 'DSUtils', 'DSErrors', - function ($rootScope, $log, $q, DSHttpAdapter, DSLocalStorageAdapter, DSUtils, DSErrors) { - - var syncMethods = require('./sync_methods'), - asyncMethods = require('./async_methods'), - cache; - - try { - cache = angular.injector(['angular-data.DSCacheFactory']).get('DSCacheFactory'); - } catch (err) { - $log.warn(err); - $log.warn('DSCacheFactory is unavailable. Resorting to the lesser capabilities of $cacheFactory.'); - cache = angular.injector(['ng']).get('$cacheFactory'); - } - - /** - * @doc interface - * @id DS - * @name DS - * @description - * Public data store interface. Consists of several properties and a number of methods. Injectable as `DS`. - * - * See the [guide](/documentation/guide/overview/index). - */ - var DS = { - $rootScope: $rootScope, - $log: $log, - $q: $q, - - cacheFactory: cache, - - /** - * @doc property - * @id DS.properties:defaults - * @name defaults - * @description - * Reference to [DSProvider.defaults](/documentation/api/api/DSProvider.properties:defaults). - */ - defaults: defaults, - - /*! - * @doc property - * @id DS.properties:store - * @name store - * @description - * Meta data for each registered resource. - */ - store: {}, - - /*! - * @doc property - * @id DS.properties:definitions - * @name definitions - * @description - * Registered resource definitions available to the data store. - */ - definitions: {}, - - /** - * @doc property - * @id DS.properties:adapters - * @name adapters - * @description - * Registered adapters available to the data store. Object consists of key-values pairs where the key is - * the name of the adapter and the value is the adapter itself. - */ - adapters: { - DSHttpAdapter: DSHttpAdapter, - DSLocalStorageAdapter: DSLocalStorageAdapter - }, - - /** - * @doc property - * @id DS.properties:errors - * @name errors - * @description - * References to the various [error types](/documentation/api/api/errors) used by angular-data. - */ - errors: DSErrors, - - /*! - * @doc property - * @id DS.properties:utils - * @name utils - * @description - * Utility functions used internally by angular-data. - */ - utils: DSUtils - }; - - DSUtils.deepFreeze(syncMethods); - DSUtils.deepFreeze(asyncMethods); - - DSUtils.deepMixIn(DS, syncMethods); - DSUtils.deepMixIn(DS, asyncMethods); - - DSUtils.deepFreeze(DS.errors); - DSUtils.deepFreeze(DS.utils); - - var $dirtyCheckScope = $rootScope.$new(); - - $dirtyCheckScope.$watch(function () { - // Throttle angular-data's digest loop to tenths of a second - // TODO: Is this okay? - return new Date().getTime() / 100 | 0; - }, function () { - DS.digest(); - }); - - return DS; - }]; + /** + * @doc property + * @id DSProvider.properties:defaults + * @name defaults + * @description + * See the [configuration guide](/documentation/guide/configure/global). + * + * Properties: + * + * - `{string}` - `baseUrl` - The url relative to which all AJAX requests will be made. + * - `{string}` - `idAttribute` - Default: `"id"` - The attribute that specifies the primary key for resources. + * - `{string}` - `defaultAdapter` - Default: `"DSHttpAdapter"` + * - `{function}` - `filter` - Default: See [angular-data query language](/documentation/guide/queries/custom). + * - `{function}` - `beforeValidate` - See [DSProvider.defaults.beforeValidate](/documentation/api/angular-data/DSProvider.properties:defaults.beforeValidate). Default: No-op + * - `{function}` - `validate` - See [DSProvider.defaults.validate](/documentation/api/angular-data/DSProvider.properties:defaults.validate). Default: No-op + * - `{function}` - `afterValidate` - See [DSProvider.defaults.afterValidate](/documentation/api/angular-data/DSProvider.properties:defaults.afterValidate). Default: No-op + * - `{function}` - `beforeCreate` - See [DSProvider.defaults.beforeCreate](/documentation/api/angular-data/DSProvider.properties:defaults.beforeCreate). Default: No-op + * - `{function}` - `afterCreate` - See [DSProvider.defaults.afterCreate](/documentation/api/angular-data/DSProvider.properties:defaults.afterCreate). Default: No-op + * - `{function}` - `beforeUpdate` - See [DSProvider.defaults.beforeUpdate](/documentation/api/angular-data/DSProvider.properties:defaults.beforeUpdate). Default: No-op + * - `{function}` - `afterUpdate` - See [DSProvider.defaults.afterUpdate](/documentation/api/angular-data/DSProvider.properties:defaults.afterUpdate). Default: No-op + * - `{function}` - `beforeDestroy` - See [DSProvider.defaults.beforeDestroy](/documentation/api/angular-data/DSProvider.properties:defaults.beforeDestroy). Default: No-op + * - `{function}` - `afterDestroy` - See [DSProvider.defaults.afterDestroy](/documentation/api/angular-data/DSProvider.properties:defaults.afterDestroy). Default: No-op + * - `{function}` - `afterInject` - See [DSProvider.defaults.afterInject](/documentation/api/angular-data/DSProvider.properties:defaults.afterInject). Default: No-op + * - `{function}` - `beforeInject` - See [DSProvider.defaults.beforeInject](/documentation/api/angular-data/DSProvider.properties:defaults.beforeInject). Default: No-op + * - `{function}` - `serialize` - See [DSProvider.defaults.serialize](/documentation/api/angular-data/DSProvider.properties:defaults.serialize). Default: No-op + * - `{function}` - `deserialize` - See [DSProvider.defaults.deserialize](/documentation/api/angular-data/DSProvider.properties:defaults.deserialize). Default: No-op + */ + var defaults = this.defaults = new Defaults(); + + this.$get = [ + '$rootScope', '$log', '$q', 'DSHttpAdapter', 'DSLocalStorageAdapter', 'DSUtils', 'DSErrors', + function ($rootScope, $log, $q, DSHttpAdapter, DSLocalStorageAdapter, DSUtils, DSErrors) { + + var syncMethods = require('./sync_methods'), + asyncMethods = require('./async_methods'), + cache; + + try { + cache = angular.injector(['angular-data.DSCacheFactory']).get('DSCacheFactory'); + } catch (err) { + $log.warn(err); + $log.warn('DSCacheFactory is unavailable. Resorting to the lesser capabilities of $cacheFactory.'); + cache = angular.injector(['ng']).get('$cacheFactory'); + } + + /** + * @doc interface + * @id DS + * @name DS + * @description + * Public data store interface. Consists of several properties and a number of methods. Injectable as `DS`. + * + * See the [guide](/documentation/guide/overview/index). + */ + var DS = { + $rootScope: $rootScope, + $log: $log, + $q: $q, + + cacheFactory: cache, + + /** + * @doc property + * @id DS.properties:defaults + * @name defaults + * @description + * Reference to [DSProvider.defaults](/documentation/api/api/DSProvider.properties:defaults). + */ + defaults: defaults, + + /*! + * @doc property + * @id DS.properties:store + * @name store + * @description + * Meta data for each registered resource. + */ + store: {}, + + /*! + * @doc property + * @id DS.properties:definitions + * @name definitions + * @description + * Registered resource definitions available to the data store. + */ + definitions: {}, + + /** + * @doc property + * @id DS.properties:adapters + * @name adapters + * @description + * Registered adapters available to the data store. Object consists of key-values pairs where the key is + * the name of the adapter and the value is the adapter itself. + */ + adapters: { + DSHttpAdapter: DSHttpAdapter, + DSLocalStorageAdapter: DSLocalStorageAdapter + }, + + /** + * @doc property + * @id DS.properties:errors + * @name errors + * @description + * References to the various [error types](/documentation/api/api/errors) used by angular-data. + */ + errors: DSErrors, + + /*! + * @doc property + * @id DS.properties:utils + * @name utils + * @description + * Utility functions used internally by angular-data. + */ + utils: DSUtils + }; + + DSUtils.deepFreeze(syncMethods); + DSUtils.deepFreeze(asyncMethods); + + DSUtils.deepMixIn(DS, syncMethods); + DSUtils.deepMixIn(DS, asyncMethods); + + DSUtils.deepFreeze(DS.errors); + DSUtils.deepFreeze(DS.utils); + + var $dirtyCheckScope = $rootScope.$new(); + + $dirtyCheckScope.$watch(function () { + // Throttle angular-data's digest loop to tenths of a second + // TODO: Is this okay? + return new Date().getTime() / 100 | 0; + }, function () { + DS.digest(); + }); + + return DS; + } + ]; } module.exports = DSProvider; -},{"../utils":60,"./async_methods":37,"./sync_methods":53}],43:[function(require,module,exports){ -var errorPrefix = 'DS.bindAll(scope, expr, resourceName, params): '; +},{"../utils":61,"./async_methods":37,"./sync_methods":54}],44:[function(require,module,exports){ +var errorPrefix = 'DS.bindAll(scope, expr, resourceName, params[, cb]): '; /** * @doc method @@ -3477,7 +3778,7 @@ var errorPrefix = 'DS.bindAll(scope, expr, resourceName, params): '; * * ## Signature: * ```js - * DS.bindAll(scope, expr, resourceName, params) + * DS.bindAll(scope, expr, resourceName, params[, cb]) * ``` * * ## Example: @@ -3485,10 +3786,8 @@ var errorPrefix = 'DS.bindAll(scope, expr, resourceName, params): '; * ```js * // bind the documents with ownerId of 5 to the 'docs' property of the $scope * var deregisterFunc = DS.bindAll($scope, 'docs', 'document', { - * query: { - * criteria: { - * ownerId: 5 - * } + * where: { + * ownerId: 5 * } * }); * ``` @@ -3496,49 +3795,61 @@ var errorPrefix = 'DS.bindAll(scope, expr, resourceName, params): '; * ## Throws * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` * * @param {object} scope The scope to bind to. * @param {string} expr An expression used to bind to the scope. Can be used to set nested keys, i.e. `"user.comments"`. * @param {string} resourceName The resource type, e.g. 'user', 'comment', etc. * @param {object} params Parameter object that is used in filtering the collection. Properties: * - * - `{object=}` - `query` - The query object by which to filter items of the type specified by `resourceName`. Properties: - * - `{object=}` - `where` - Where clause. - * - `{number=}` - `limit` - Limit clause. - * - `{skip=}` - `skip` - Skip clause. - * - `{orderBy=}` - `orderBy` - OrderBy clause. + * - `{object=}` - `where` - Where clause. + * - `{number=}` - `limit` - Limit clause. + * - `{number=}` - `skip` - Skip clause. + * - `{number=}` - `offset` - Same as skip. + * - `{string|array=}` - `orderBy` - OrderBy clause. + * + * @param {function=} cb Optional callback executed on change. Signature: `cb(err, items)`. + * * @returns {function} Scope $watch deregistration function. */ -function bindOne(scope, expr, resourceName, params) { - if (!this.utils.isObject(scope)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'scope: Must be an object!'); - } else if (!this.utils.isString(expr)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'expr: Must be a string!'); - } else if (!this.definitions[resourceName]) { - throw new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!'); - } else if (!this.utils.isObject(params)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'params: Must be an object!'); - } - - var _this = this; - - try { - return scope.$watch(function () { - return _this.lastModified(resourceName); - }, function () { - _this.utils.set(scope, expr, _this.filter(resourceName, params)); - }); - } catch (err) { - throw new this.errors.UnhandledError(err); - } +function bindOne(scope, expr, resourceName, params, cb) { + var IA = this.errors.IA; + + if (!this.utils.isObject(scope)) { + throw new IA(errorPrefix + 'scope: Must be an object!'); + } else if (!this.utils.isString(expr)) { + throw new IA(errorPrefix + 'expr: Must be a string!'); + } else if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isObject(params)) { + throw new IA(errorPrefix + 'params: Must be an object!'); + } + + var _this = this; + + try { + return scope.$watch(function () { + return _this.lastModified(resourceName); + }, function () { + var items = _this.filter(resourceName, params); + _this.utils.set(scope, expr, items); + if (cb) { + cb(null, items); + } + }); + } catch (err) { + if (cb) { + cb(err); + } else { + throw err; + } + } } module.exports = bindOne; -},{}],44:[function(require,module,exports){ -var errorPrefix = 'DS.bindOne(scope, expr, resourceName, id): '; +},{}],45:[function(require,module,exports){ +var errorPrefix = 'DS.bindOne(scope, expr, resourceName, id[, cb]): '; /** * @doc method @@ -3549,7 +3860,7 @@ var errorPrefix = 'DS.bindOne(scope, expr, resourceName, id): '; * * ## Signature: * ```js - * DS.bindOne(scope, expr, resourceName, id) + * DS.bindOne(scope, expr, resourceName, id[, cb]) * ``` * * ## Example: @@ -3562,42 +3873,52 @@ var errorPrefix = 'DS.bindOne(scope, expr, resourceName, id): '; * ## Throws * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` * * @param {object} scope The scope to bind to. * @param {string} expr An expression used to bind to the scope. Can be used to set nested keys, i.e. `"user.profile"`. * @param {string} resourceName The resource type, e.g. 'user', 'comment', etc. * @param {string|number} id The primary key of the item to bind. + * @param {function=} cb Optional callback executed on change. Signature: `cb(err, item)`. * @returns {function} Scope $watch deregistration function. */ -function bindOne(scope, expr, resourceName, id) { - if (!this.utils.isObject(scope)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'scope: Must be an object!'); - } else if (!this.utils.isString(expr)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'expr: Must be a string!'); - } else if (!this.definitions[resourceName]) { - throw new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!'); - } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'id: Must be a string or a number!'); - } - - var _this = this; - - try { - return scope.$watch(function () { - return _this.lastModified(resourceName, id); - }, function () { - _this.utils.set(scope, expr, _this.get(resourceName, id)); - }); - } catch (err) { - throw new this.errors.UnhandledError(err); - } +function bindOne(scope, expr, resourceName, id, cb) { + var IA = this.errors.IA; + + if (!this.utils.isObject(scope)) { + throw new IA(errorPrefix + 'scope: Must be an object!'); + } else if (!this.utils.isString(expr)) { + throw new IA(errorPrefix + 'expr: Must be a string!'); + } else if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { + throw new IA(errorPrefix + 'id: Must be a string or a number!'); + } + + var _this = this; + + try { + return scope.$watch(function () { + return _this.lastModified(resourceName, id); + }, function () { + var item = _this.get(resourceName, id); + _this.utils.set(scope, expr, item); + if (cb) { + cb(null, item); + } + }); + } catch (err) { + if (cb) { + cb(err); + } else { + throw err; + } + } } module.exports = bindOne; -},{}],45:[function(require,module,exports){ +},{}],46:[function(require,module,exports){ var errorPrefix = 'DS.changes(resourceName, id): '; /** @@ -3627,46 +3948,41 @@ var errorPrefix = 'DS.changes(resourceName, id): '; * ## Throws * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` * * @param {string} resourceName The resource type, e.g. 'user', 'comment', etc. * @param {string|number} id The primary key of the item of the changes to retrieve. * @returns {object} The changes of the item of the type specified by `resourceName` with the primary key specified by `id`. */ function changes(resourceName, id) { - if (!this.definitions[resourceName]) { - throw new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!'); - } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'id: Must be a string or a number!', { id: { actual: typeof id, expected: 'string|number' } }); - } - - try { - var item = this.get(resourceName, id); - if (item) { - this.store[resourceName].observers[id].deliver(); - return this.utils.diffObjectFromOldObject(item, this.store[resourceName].previousAttributes[id]); - } - } catch (err) { - throw new this.errors.UnhandledError(err); - } + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { + throw new this.errors.IA(errorPrefix + 'id: Must be a string or a number!'); + } + + var item = this.get(resourceName, id); + if (item) { + this.store[resourceName].observers[id].deliver(); + return this.utils.diffObjectFromOldObject(item, this.store[resourceName].previousAttributes[id]); + } } module.exports = changes; -},{}],46:[function(require,module,exports){ +},{}],47:[function(require,module,exports){ /*jshint evil:true*/ var errorPrefix = 'DS.defineResource(definition): '; function Resource(utils, options) { - utils.deepMixIn(this, options); + utils.deepMixIn(this, options); - if ('endpoint' in options) { - this.endpoint = options.endpoint; - } else { - this.endpoint = this.name; - } + if ('endpoint' in options) { + this.endpoint = options.endpoint; + } else { + this.endpoint = this.name; + } } /** @@ -3700,7 +4016,6 @@ function Resource(utils, options) { * * - `{IllegalArgumentError}` * - `{RuntimeError}` - * - `{UnhandledError}` * * @param {string|object} definition Name of resource or resource definition object: Properties: * @@ -3730,73 +4045,93 @@ function Resource(utils, options) { * See [DSProvider.defaults](/documentation/api/angular-data/DSProvider.properties:defaults). */ function defineResource(definition) { - if (this.utils.isString(definition)) { - definition = { - name: definition - }; - } - if (!this.utils.isObject(definition)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'definition: Must be an object!', { definition: { actual: typeof definition, expected: 'object' } }); - } else if (!this.utils.isString(definition.name)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'definition.name: Must be a string!', { definition: { name: { actual: typeof definition.name, expected: 'string' } } }); - } else if (definition.idAttribute && !this.utils.isString(definition.idAttribute)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'definition.idAttribute: Must be a string!', { definition: { idAttribute: { actual: typeof definition.idAttribute, expected: 'string' } } }); - } else if (definition.endpoint && !this.utils.isString(definition.endpoint)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'definition.endpoint: Must be a string!', { definition: { endpoint: { actual: typeof definition.endpoint, expected: 'string' } } }); - } else if (this.store[definition.name]) { - throw new this.errors.RuntimeError(errorPrefix + definition.name + ' is already registered!'); - } + var IA = this.errors.IA; - try { - Resource.prototype = this.defaults; - this.definitions[definition.name] = new Resource(this.utils, definition); - - var _this = this, - def = this.definitions[definition.name]; - - var cache = this.cacheFactory('DS.' + def.name, { - maxAge: def.maxAge || null, - recycleFreq: def.recycleFreq || 1000, - cacheFlushInterval: def.cacheFlushInterval || null, - deleteOnExpire: def.deleteOnExpire || 'none', - onExpire: function (id) { - _this.eject(def.name, id); - }, - capacity: Number.MAX_VALUE, - storageMode: 'memory', - storageImpl: null, - disabled: false, - storagePrefix: 'DS.' + def.name - }); - - if (def.methods) { - def.class = definition.name[0].toUpperCase() + definition.name.substring(1); - eval('function ' + def.class + '() {}'); - def[def.class] = eval(def.class); - this.utils.deepMixIn(def[def.class].prototype, def.methods); - } + if (this.utils.isString(definition)) { + definition = { + name: definition + }; + } + if (!this.utils.isObject(definition)) { + throw new IA(errorPrefix + 'definition: Must be an object!'); + } else if (!this.utils.isString(definition.name)) { + throw new IA(errorPrefix + 'definition.name: Must be a string!'); + } else if (definition.idAttribute && !this.utils.isString(definition.idAttribute)) { + throw new IA(errorPrefix + 'definition.idAttribute: Must be a string!'); + } else if (definition.endpoint && !this.utils.isString(definition.endpoint)) { + throw new IA(errorPrefix + 'definition.endpoint: Must be a string!'); + } else if (this.store[definition.name]) { + throw new this.errors.R(errorPrefix + definition.name + ' is already registered!'); + } + + try { + Resource.prototype = this.defaults; + this.definitions[definition.name] = new Resource(this.utils, definition); + + var _this = this; + var def = this.definitions[definition.name]; + + var cache = this.cacheFactory('DS.' + def.name, { + maxAge: def.maxAge || null, + recycleFreq: def.recycleFreq || 1000, + cacheFlushInterval: def.cacheFlushInterval || null, + deleteOnExpire: def.deleteOnExpire || 'none', + onExpire: function (id) { + _this.eject(def.name, id); + }, + capacity: Number.MAX_VALUE, + storageMode: 'memory', + storageImpl: null, + disabled: false, + storagePrefix: 'DS.' + def.name + }); + + if (def.methods) { + def.class = definition.name[0].toUpperCase() + definition.name.substring(1); + eval('function ' + def.class + '() {}'); + def[def.class] = eval(def.class); + this.utils.deepMixIn(def[def.class].prototype, def.methods); + } - this.store[def.name] = { - collection: [], - completedQueries: {}, - pendingQueries: {}, - index: cache, - modified: {}, - saved: {}, - previousAttributes: {}, - observers: {}, - collectionModified: 0 - }; - } catch (err) { - delete this.definitions[definition.name]; - delete this.store[definition.name]; - throw new this.errors.UnhandledError(err); - } + if (def.computed) { + this.utils.forOwn(def.computed, function (fn, field) { + if (def.methods && field in def.methods) { + _this.$log.warn(errorPrefix + 'Computed property "' + field + '" conflicts with previously defined prototype method!'); + } + var match = fn.toString().match(/function.*?\(([\s\S]*?)\)/); + var deps = match[1].split(','); + fn.deps = _this.utils.filter(deps, function (dep) { + return !!dep; + }); + angular.forEach(fn.deps, function (val, index) { + fn.deps[index] = val.trim(); + }); + }); + } + + this.store[def.name] = { + collection: [], + completedQueries: {}, + pendingQueries: {}, + index: cache, + modified: {}, + saved: {}, + previousAttributes: {}, + observers: {}, + collectionModified: 0 + }; + } + catch + (err) { + delete this.definitions[definition.name]; + delete this.store[definition.name]; + throw err; + } } module.exports = defineResource; -},{}],47:[function(require,module,exports){ +},{}],48:[function(require,module,exports){ var observe = require('../../../lib/observe-js/observe-js'); /** @@ -3818,47 +4153,40 @@ var observe = require('../../../lib/observe-js/observe-js'); * Works like $scope.$apply() * ``` * - * ## Throws - * - * - `{UnhandledError}` */ function digest() { - try { - if (!this.$rootScope.$$phase) { - this.$rootScope.$apply(function () { - observe.Platform.performMicrotaskCheckpoint(); - }); - } else { - observe.Platform.performMicrotaskCheckpoint(); - } - } catch (err) { - throw new this.errors.UnhandledError(err); - } + if (!this.$rootScope.$$phase) { + this.$rootScope.$apply(function () { + observe.Platform.performMicrotaskCheckpoint(); + }); + } else { + observe.Platform.performMicrotaskCheckpoint(); + } } module.exports = digest; -},{"../../../lib/observe-js/observe-js":1}],48:[function(require,module,exports){ +},{"../../../lib/observe-js/observe-js":1}],49:[function(require,module,exports){ var errorPrefix = 'DS.eject(resourceName, id): '; function _eject(definition, resource, id) { - var found = false; - for (var i = 0; i < resource.collection.length; i++) { - if (resource.collection[i][definition.idAttribute] == id) { - found = true; - break; - } - } - if (found) { - resource.collection.splice(i, 1); - resource.observers[id].close(); - delete resource.observers[id]; - - resource.index.remove(id); - delete resource.previousAttributes[id]; - delete resource.modified[id]; - delete resource.saved[id]; - } + var found = false; + for (var i = 0; i < resource.collection.length; i++) { + if (resource.collection[i][definition.idAttribute] == id) { + found = true; + break; + } + } + if (found) { + resource.collection.splice(i, 1); + resource.observers[id].close(); + delete resource.observers[id]; + + resource.index.remove(id); + delete resource.previousAttributes[id]; + delete resource.modified[id]; + delete resource.saved[id]; + } } /** @@ -3887,52 +4215,49 @@ function _eject(definition, resource, id) { * ## Throws * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` * * @param {string} resourceName The resource type, e.g. 'user', 'comment', etc. * @param {string|number} id The primary key of the item to eject. */ function eject(resourceName, id) { - if (!this.definitions[resourceName]) { - throw new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!'); - } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'id: Must be a string or a number!', { id: { actual: typeof id, expected: 'string|number' } }); - } - - var resource = this.store[resourceName], - _this = this; - - try { - if (!this.$rootScope.$$phase) { - this.$rootScope.$apply(function () { - _eject(_this.definitions[resourceName], resource, id); - resource.collectionModified = _this.utils.updateTimestamp(resource.collectionModified); - }); - } else { - _eject(_this.definitions[resourceName], resource, id); - resource.collectionModified = _this.utils.updateTimestamp(resource.collectionModified); - } - delete this.store[resourceName].completedQueries[id]; - } catch (err) { - throw new this.errors.UnhandledError(err); - } + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { + throw new this.errors.IA(errorPrefix + 'id: Must be a string or a number!'); + } + + var resource = this.store[resourceName]; + var _this = this; + + if (!this.$rootScope.$$phase) { + this.$rootScope.$apply(function () { + _eject(_this.definitions[resourceName], resource, id); + resource.collectionModified = _this.utils.updateTimestamp(resource.collectionModified); + }); + } else { + _eject(_this.definitions[resourceName], resource, id); + resource.collectionModified = _this.utils.updateTimestamp(resource.collectionModified); + } + delete this.store[resourceName].completedQueries[id]; } module.exports = eject; -},{}],49:[function(require,module,exports){ +},{}],50:[function(require,module,exports){ var errorPrefix = 'DS.ejectAll(resourceName[, params]): '; function _ejectAll(definition, resource, params) { - var queryHash = this.utils.toJson(params), - items = this.filter(definition.name, params); + var queryHash = this.utils.toJson(params); + var items = this.filter(definition.name, params); + var ids = this.utils.toLookup(items, definition.idAttribute); + var _this = this; - for (var i = 0; i < items.length; i++) { - this.eject(definition.name, items[i][definition.idAttribute]); - } + angular.forEach(ids, function (item, id) { + _this.eject(definition.name, id); + }); - delete resource.completedQueries[queryHash]; + delete resource.completedQueries[queryHash]; } /** @@ -3965,7 +4290,7 @@ function _ejectAll(definition, resource, params) { * DS.filter('document'); // [ { title: 'How to Cook', id: 45, author: 'John Anderson' }, * // { title: 'How to Eat', id: 46, author: 'Sally Jane' } ] * - * DS.ejectAll('document', { query: { where: { author: 'Sally Jane' } } }); + * DS.ejectAll('document', { where: { author: 'Sally Jane' } }); * * DS.filter('document'); // [ { title: 'How to Cook', id: 45, author: 'John Anderson' } ] * ``` @@ -3984,50 +4309,51 @@ function _ejectAll(definition, resource, params) { * ## Throws * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` * * @param {string} resourceName The resource type, e.g. 'user', 'comment', etc. * @param {object} params Parameter object that is serialized into the query string. Properties: * - * - `{object=}` - `query` - The query object by which to filter items of the type specified by `resourceName`. Properties: - * - `{object=}` - `where` - Where clause. - * - `{number=}` - `limit` - Limit clause. - * - `{skip=}` - `skip` - Skip clause. - * - `{orderBy=}` - `orderBy` - OrderBy clause. + * - `{object=}` - `where` - Where clause. + * - `{number=}` - `limit` - Limit clause. + * - `{number=}` - `skip` - Skip clause. + * - `{number=}` - `offset` - Same as skip. + * - `{string|array=}` - `orderBy` - OrderBy clause. */ function ejectAll(resourceName, params) { - params = params || {}; - - if (!this.definitions[resourceName]) { - throw new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!'); - } else if (!this.utils.isObject(params)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'params: Must be an object!', { params: { actual: typeof params, expected: 'object' } }); - } - - var resource = this.store[resourceName], - _this = this; - - try { - if (!this.$rootScope.$$phase) { - this.$rootScope.$apply(function () { - _ejectAll.apply(_this, [_this.definitions[resourceName], resource, params]); - resource.collectionModified = _this.utils.updateTimestamp(resource.collectionModified); - }); - } else { - _ejectAll.apply(_this, [_this.definitions[resourceName], resource, params]); - resource.collectionModified = this.utils.updateTimestamp(resource.collectionModified); - } - } catch (err) { - throw new this.errors.UnhandledError(err); - } + params = params || {}; + + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isObject(params)) { + throw new this.errors.IA(errorPrefix + 'params: Must be an object!'); + } + + var _this = this; + var resource = this.store[resourceName]; + var queryHash = this.utils.toJson(params); + + delete resource.completedQueries[queryHash]; + + if (this.utils.isEmpty(params)) { + resource.completedQueries = {}; + } + + if (!this.$rootScope.$$phase) { + this.$rootScope.$apply(function () { + _ejectAll.apply(_this, [_this.definitions[resourceName], resource, params]); + resource.collectionModified = _this.utils.updateTimestamp(resource.collectionModified); + }); + } else { + _ejectAll.apply(_this, [_this.definitions[resourceName], resource, params]); + resource.collectionModified = this.utils.updateTimestamp(resource.collectionModified); + } } module.exports = ejectAll; -},{}],50:[function(require,module,exports){ -/* jshint loopfunc: true */ -var errorPrefix = 'DS.filter(resourceName, params[, options]): '; +},{}],51:[function(require,module,exports){ +var errorPrefix = 'DS.filter(resourceName[, params][, options]): '; /** * @doc method @@ -4038,7 +4364,7 @@ var errorPrefix = 'DS.filter(resourceName, params[, options]): '; * * ## Signature: * ```js - * DS.filter(resourceName, params[, options]) + * DS.filter(resourceName[, params][, options]) * ``` * * ## Example: @@ -4050,140 +4376,64 @@ var errorPrefix = 'DS.filter(resourceName, params[, options]): '; * ## Throws * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` * * @param {string} resourceName The resource type, e.g. 'user', 'comment', etc. * @param {object=} params Parameter object that is serialized into the query string. Properties: * - * - `{object=}` - `query` - The query object by which to filter items of the type specified by `resourceName`. Properties: - * - `{object=}` - `where` - Where clause. - * - `{number=}` - `limit` - Limit clause. - * - `{skip=}` - `skip` - Skip clause. - * - `{orderBy=}` - `orderBy` - OrderBy clause. + * - `{object=}` - `where` - Where clause. + * - `{number=}` - `limit` - Limit clause. + * - `{number=}` - `skip` - Skip clause. + * - `{number=}` - `offset` - Same as skip. + * - `{string|array=}` - `orderBy` - OrderBy clause. * * @param {object=} options Optional configuration. Properties: * - `{boolean=}` - `loadFromServer` - Send the query to server if it has not been sent yet. Default: `false`. + * - `{boolean=}` - `allowSimpleWhere` - Treat top-level fields on the `params` argument as simple "where" equality clauses. Default: `true`. * @returns {array} The filtered collection of items of the type specified by `resourceName`. */ function filter(resourceName, params, options) { - options = options || {}; - - if (!this.definitions[resourceName]) { - throw new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!'); - } else if (params && !this.utils.isObject(params)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'params: Must be an object!', { params: { actual: typeof params, expected: 'object' } }); - } else if (!this.utils.isObject(options)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'options: Must be an object!', { options: { actual: typeof options, expected: 'object' } }); - } + var IA = this.errors.IA; - try { - var definition = this.definitions[resourceName], - resource = this.store[resourceName], - _this = this; + options = options || {}; - // Protect against null - params = params || {}; + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (params && !this.utils.isObject(params)) { + throw new IA(errorPrefix + 'params: Must be an object!'); + } else if (!this.utils.isObject(options)) { + throw new IA(errorPrefix + 'options: Must be an object!'); + } - var queryHash = this.utils.toJson(params); + var definition = this.definitions[resourceName]; + var resource = this.store[resourceName]; - if (!(queryHash in resource.completedQueries) && options.loadFromServer) { - // This particular query has never been completed + // Protect against null + params = params || {}; - if (!resource.pendingQueries[queryHash]) { - // This particular query has never even been started - this.findAll(resourceName, params, options); - } - } + if ('allowSimpleWhere' in options) { + options.allowSimpleWhere = !!options.allowSimpleWhere; + } else { + options.allowSimpleWhere = true; + } - params.query = params.query || {}; - // The query has been completed, so hit the cache with the query - var filtered = this.utils.filter(resource.collection, function (attrs) { - var keep = true, - where = params.query.where; + var queryHash = this.utils.toJson(params); - // Apply 'where' clauses - if (where) { - if (!_this.utils.isObject(where)) { - throw new _this.errors.IllegalArgumentError(errorPrefix + 'params.query.where: Must be an object!', { params: { query: { where: { actual: typeof params.query.where, expected: 'object' } } } }); - } - keep = definition.filter(resourceName, where, attrs); - } - return keep; - }); - - // Apply 'orderBy' - if (params.query.orderBy) { - if (this.utils.isString(params.query.orderBy)) { - params.query.orderBy = [ - [params.query.orderBy, 'ASC'] - ]; - } - if (this.utils.isArray(params.query.orderBy)) { - for (var i = 0; i < params.query.orderBy.length; i++) { - if (this.utils.isString(params.query.orderBy[i])) { - params.query.orderBy[i] = [params.query.orderBy[i], 'ASC']; - } else if (!this.utils.isArray(params.query.orderBy[i])) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'params.query.orderBy[' + i + ']: Must be a string or an array!', { params: { query: { 'orderBy[i]': { actual: typeof params.query.orderBy[i], expected: 'string|array' } } } }); - } - filtered = this.utils.sort(filtered, function (a, b) { - var cA = a[params.query.orderBy[i][0]], cB = b[params.query.orderBy[i][0]]; - if (_this.utils.isString(cA)) { - cA = _this.utils.upperCase(cA); - } - if (_this.utils.isString(cB)) { - cB = _this.utils.upperCase(cB); - } - if (params.query.orderBy[i][1] === 'DESC') { - if (cB < cA) { - return -1; - } else if (cB > cA) { - return 1; - } else { - return 0; - } - } else { - if (cA < cB) { - return -1; - } else if (cA > cB) { - return 1; - } else { - return 0; - } - } - }); - } - } else { - throw new this.errors.IllegalArgumentError(errorPrefix + 'params.query.orderBy: Must be a string or an array!', { params: { query: { orderBy: { actual: typeof params.query.orderBy, expected: 'string|array' } } } }); - } - } + if (!(queryHash in resource.completedQueries) && options.loadFromServer) { + // This particular query has never been completed - // Apply 'limit' and 'skip' - if (this.utils.isNumber(params.query.limit) && this.utils.isNumber(params.query.skip)) { - filtered = this.utils.slice(filtered, params.query.skip, Math.min(filtered.length, params.query.skip + params.query.limit)); - } else if (this.utils.isNumber(params.query.limit)) { - filtered = this.utils.slice(filtered, 0, Math.min(filtered.length, params.query.limit)); - } else if (this.utils.isNumber(params.query.skip)) { - if (params.query.skip < filtered.length) { - filtered = this.utils.slice(filtered, params.query.skip); - } else { - filtered = []; - } - } + if (!resource.pendingQueries[queryHash]) { + // This particular query has never even been started + this.findAll(resourceName, params, options); + } + } - return filtered; - } catch (err) { - if (err instanceof this.errors.IllegalArgumentError) { - throw err; - } else { - throw new this.errors.UnhandledError(err); - } - } + return definition.filter.call(this, resource.collection, resourceName, params, options); } module.exports = filter; -},{}],51:[function(require,module,exports){ +},{}],52:[function(require,module,exports){ var errorPrefix = 'DS.get(resourceName, id[, options]): '; /** @@ -4208,8 +4458,7 @@ var errorPrefix = 'DS.get(resourceName, id[, options]): '; * ## Throws * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` * * @param {string} resourceName The resource type, e.g. 'user', 'comment', etc. * @param {string|number} id The primary key of the item to retrieve. @@ -4218,42 +4467,40 @@ var errorPrefix = 'DS.get(resourceName, id[, options]): '; * @returns {object} The item of the type specified by `resourceName` with the primary key specified by `id`. */ function get(resourceName, id, options) { - options = options || {}; - - if (!this.definitions[resourceName]) { - throw new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!'); - } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'id: Must be a string or a number!', { id: { actual: typeof id, expected: 'string|number' } }); - } else if (!this.utils.isObject(options)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'options: Must be an object!', { options: { actual: typeof options, expected: 'object' } }); - } - var _this = this; - - try { - // cache miss, request resource from server - var item = this.store[resourceName].index.get(id); - if (!item && options.loadFromServer) { - this.find(resourceName, id).then(null, function (err) { - return _this.$q.reject(err); - }); - } - - // return resource from cache - return item; - } catch (err) { - throw new this.errors.UnhandledError(err); - } + var IA = this.errors.IA; + + options = options || {}; + + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { + throw new IA(errorPrefix + 'id: Must be a string or a number!'); + } else if (!this.utils.isObject(options)) { + throw new IA(errorPrefix + 'options: Must be an object!'); + } + var _this = this; + + // cache miss, request resource from server + var item = this.store[resourceName].index.get(id); + if (!item && options.loadFromServer) { + this.find(resourceName, id).then(null, function (err) { + return _this.$q.reject(err); + }); + } + + // return resource from cache + return item; } module.exports = get; -},{}],52:[function(require,module,exports){ +},{}],53:[function(require,module,exports){ var errorPrefix = 'DS.hasChanges(resourceName, id): '; function diffIsEmpty(utils, diff) { - return !(utils.isEmpty(diff.added) && - utils.isEmpty(diff.removed) && - utils.isEmpty(diff.changed)); + return !(utils.isEmpty(diff.added) && + utils.isEmpty(diff.removed) && + utils.isEmpty(diff.changed)); } /** @@ -4282,259 +4529,294 @@ function diffIsEmpty(utils, diff) { * ## Throws * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` * * @param {string} resourceName The resource type, e.g. 'user', 'comment', etc. * @param {string|number} id The primary key of the item. * @returns {boolean} Whether the item of the type specified by `resourceName` with the primary key specified by `id` has changes. */ function hasChanges(resourceName, id) { - if (!this.definitions[resourceName]) { - throw new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!'); - } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'id: Must be a string or a number!', { id: { actual: typeof id, expected: 'string|number' } }); - } - - try { - // return resource from cache - if (this.get(resourceName, id)) { - return diffIsEmpty(this.utils, this.changes(resourceName, id)); - } else { - return false; - } - } catch (err) { - throw new this.errors.UnhandledError(err); - } + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { + throw new this.errors.IA(errorPrefix + 'id: Must be a string or a number!'); + } + + // return resource from cache + if (this.get(resourceName, id)) { + return diffIsEmpty(this.utils, this.changes(resourceName, id)); + } else { + return false; + } } module.exports = hasChanges; -},{}],53:[function(require,module,exports){ +},{}],54:[function(require,module,exports){ module.exports = { - /** - * @doc method - * @id DS.sync_methods:defineResource - * @name defineResource - * @methodOf DS - * @description - * See [DS.defineResource](/documentation/api/api/DS.sync_methods:defineResource). - */ - defineResource: require('./defineResource'), - - /** - * @doc method - * @id DS.sync_methods:bindOne - * @name bindOne - * @methodOf DS - * @description - * See [DS.bindOne](/documentation/api/api/DS.sync_methods:bindOne). - */ - bindOne: require('./bindOne'), - - /** - * @doc method - * @id DS.sync_methods:bindAll - * @name bindAll - * @methodOf DS - * @description - * See [DS.bindAll](/documentation/api/api/DS.sync_methods:bindAll). - */ - bindAll: require('./bindAll'), - - /** - * @doc method - * @id DS.sync_methods:eject - * @name eject - * @methodOf DS - * @description - * See [DS.eject](/documentation/api/api/DS.sync_methods:eject). - */ - eject: require('./eject'), - - /** - * @doc method - * @id DS.sync_methods:ejectAll - * @name ejectAll - * @methodOf DS - * @description - * See [DS.ejectAll](/documentation/api/api/DS.sync_methods:ejectAll). - */ - ejectAll: require('./ejectAll'), - - /** - * @doc method - * @id DS.sync_methods:filter - * @name filter - * @methodOf DS - * @description - * See [DS.filter](/documentation/api/api/DS.sync_methods:filter). - */ - filter: require('./filter'), - - /** - * @doc method - * @id DS.sync_methods:get - * @name get - * @methodOf DS - * @description - * See [DS.get](/documentation/api/api/DS.sync_methods:get). - */ - get: require('./get'), - - /** - * @doc method - * @id DS.sync_methods:inject - * @name inject - * @methodOf DS - * @description - * See [DS.inject](/documentation/api/api/DS.sync_methods:inject). - */ - inject: require('./inject'), - - /** - * @doc method - * @id DS.sync_methods:lastModified - * @name lastModified - * @methodOf DS - * @description - * See [DS.lastModified](/documentation/api/api/DS.sync_methods:lastModified). - */ - lastModified: require('./lastModified'), - - /** - * @doc method - * @id DS.sync_methods:lastSaved - * @name lastSaved - * @methodOf DS - * @description - * See [DS.lastSaved](/documentation/api/api/DS.sync_methods:lastSaved). - */ - lastSaved: require('./lastSaved'), - - /** - * @doc method - * @id DS.sync_methods:digest - * @name digest - * @methodOf DS - * @description - * See [DS.digest](/documentation/api/api/DS.sync_methods:digest). - */ - digest: require('./digest'), - - /** - * @doc method - * @id DS.sync_methods:changes - * @name changes - * @methodOf DS - * @description - * See [DS.changes](/documentation/api/api/DS.sync_methods:changes). - */ - changes: require('./changes'), - - /** - * @doc method - * @id DS.sync_methods:previous - * @name previous - * @methodOf DS - * @description - * See [DS.previous](/documentation/api/api/DS.sync_methods:previous). - */ - previous: require('./previous'), - - /** - * @doc method - * @id DS.sync_methods:hasChanges - * @name hasChanges - * @methodOf DS - * @description - * See [DS.hasChanges](/documentation/api/api/DS.sync_methods:hasChanges). - */ - hasChanges: require('./hasChanges') + /** + * @doc method + * @id DS.sync_methods:defineResource + * @name defineResource + * @methodOf DS + * @description + * See [DS.defineResource](/documentation/api/api/DS.sync_methods:defineResource). + */ + defineResource: require('./defineResource'), + + /** + * @doc method + * @id DS.sync_methods:bindOne + * @name bindOne + * @methodOf DS + * @description + * See [DS.bindOne](/documentation/api/api/DS.sync_methods:bindOne). + */ + bindOne: require('./bindOne'), + + /** + * @doc method + * @id DS.sync_methods:bindAll + * @name bindAll + * @methodOf DS + * @description + * See [DS.bindAll](/documentation/api/api/DS.sync_methods:bindAll). + */ + bindAll: require('./bindAll'), + + /** + * @doc method + * @id DS.sync_methods:eject + * @name eject + * @methodOf DS + * @description + * See [DS.eject](/documentation/api/api/DS.sync_methods:eject). + */ + eject: require('./eject'), + + /** + * @doc method + * @id DS.sync_methods:ejectAll + * @name ejectAll + * @methodOf DS + * @description + * See [DS.ejectAll](/documentation/api/api/DS.sync_methods:ejectAll). + */ + ejectAll: require('./ejectAll'), + + /** + * @doc method + * @id DS.sync_methods:filter + * @name filter + * @methodOf DS + * @description + * See [DS.filter](/documentation/api/api/DS.sync_methods:filter). + */ + filter: require('./filter'), + + /** + * @doc method + * @id DS.sync_methods:get + * @name get + * @methodOf DS + * @description + * See [DS.get](/documentation/api/api/DS.sync_methods:get). + */ + get: require('./get'), + + /** + * @doc method + * @id DS.sync_methods:inject + * @name inject + * @methodOf DS + * @description + * See [DS.inject](/documentation/api/api/DS.sync_methods:inject). + */ + inject: require('./inject'), + + /** + * @doc method + * @id DS.sync_methods:lastModified + * @name lastModified + * @methodOf DS + * @description + * See [DS.lastModified](/documentation/api/api/DS.sync_methods:lastModified). + */ + lastModified: require('./lastModified'), + + /** + * @doc method + * @id DS.sync_methods:lastSaved + * @name lastSaved + * @methodOf DS + * @description + * See [DS.lastSaved](/documentation/api/api/DS.sync_methods:lastSaved). + */ + lastSaved: require('./lastSaved'), + + /** + * @doc method + * @id DS.sync_methods:digest + * @name digest + * @methodOf DS + * @description + * See [DS.digest](/documentation/api/api/DS.sync_methods:digest). + */ + digest: require('./digest'), + + /** + * @doc method + * @id DS.sync_methods:changes + * @name changes + * @methodOf DS + * @description + * See [DS.changes](/documentation/api/api/DS.sync_methods:changes). + */ + changes: require('./changes'), + + /** + * @doc method + * @id DS.sync_methods:previous + * @name previous + * @methodOf DS + * @description + * See [DS.previous](/documentation/api/api/DS.sync_methods:previous). + */ + previous: require('./previous'), + + /** + * @doc method + * @id DS.sync_methods:hasChanges + * @name hasChanges + * @methodOf DS + * @description + * See [DS.hasChanges](/documentation/api/api/DS.sync_methods:hasChanges). + */ + hasChanges: require('./hasChanges') }; -},{"./bindAll":43,"./bindOne":44,"./changes":45,"./defineResource":46,"./digest":47,"./eject":48,"./ejectAll":49,"./filter":50,"./get":51,"./hasChanges":52,"./inject":54,"./lastModified":55,"./lastSaved":56,"./previous":57}],54:[function(require,module,exports){ -var observe = require('../../../lib/observe-js/observe-js'), - errorPrefix = 'DS.inject(resourceName, attrs[, options]): '; +},{"./bindAll":44,"./bindOne":45,"./changes":46,"./defineResource":47,"./digest":48,"./eject":49,"./ejectAll":50,"./filter":51,"./get":52,"./hasChanges":53,"./inject":55,"./lastModified":56,"./lastSaved":57,"./previous":58}],55:[function(require,module,exports){ +var observe = require('../../../lib/observe-js/observe-js'); +var errorPrefix = 'DS.inject(resourceName, attrs[, options]): '; function _inject(definition, resource, attrs) { - var _this = this, - $log = _this.$log; - - function _react(added, removed, changed, getOldValueFn) { - try { - var innerId = getOldValueFn(definition.idAttribute); + var _this = this; + var $log = _this.$log; + + function _react(added, removed, changed, getOldValueFn) { + var innerId = getOldValueFn(definition.idAttribute); + + resource.modified[innerId] = _this.utils.updateTimestamp(resource.modified[innerId]); + resource.collectionModified = _this.utils.updateTimestamp(resource.collectionModified); + + if (definition.computed) { + var item = _this.get(definition.name, innerId); + _this.utils.forOwn(definition.computed, function (fn, field) { + var compute = false; + // check if required fields changed + angular.forEach(fn.deps, function (dep) { + if (dep in changed || dep in removed || dep in changed || !(field in item)) { + compute = true; + } + }); + if (compute) { + var args = []; + angular.forEach(fn.deps, function (dep) { + args.push(item[dep]); + }); + // recompute property + item[field] = fn.apply(item, args); + } + }); + } - resource.modified[innerId] = _this.utils.updateTimestamp(resource.modified[innerId]); - resource.collectionModified = _this.utils.updateTimestamp(resource.collectionModified); + if (definition.idAttribute in changed) { + $log.error('Doh! You just changed the primary key of an object! ' + + 'I don\'t know how to handle this yet, so your data for the "' + definition.name + + '" resource is now in an undefined (probably broken) state.'); + } + } - if (definition.idAttribute in changed) { - $log.error('Doh! You just changed the primary key of an object! ' + - 'I don\'t know how to handle this yet, so your data for the "' + definition.name + - '" resource is now in an undefined (probably broken) state.'); - } - } catch (err) { - throw new _this.errors.UnhandledError(err); - } - } + var injected; + if (_this.utils.isArray(attrs)) { + injected = []; + for (var i = 0; i < attrs.length; i++) { + injected.push(_inject.call(_this, definition, resource, attrs[i])); + } + } else { + // check if "idAttribute" is a computed property + if (definition.computed && definition.computed[definition.idAttribute]) { + var args = []; + angular.forEach(definition.computed[definition.idAttribute].deps, function (dep) { + args.push(attrs[dep]); + }); + attrs[definition.idAttribute] = definition.computed[definition.idAttribute].apply(attrs, args); + } + if (!(definition.idAttribute in attrs)) { + throw new _this.errors.R(errorPrefix + 'attrs: Must contain the property specified by `idAttribute`!'); + } else { + try { + definition.beforeInject(definition.name, attrs); + var id = attrs[definition.idAttribute], + item = this.get(definition.name, id); + + if (!item) { + if (definition.class) { + if (attrs instanceof definition[definition.class]) { + item = attrs; + } else { + item = new definition[definition.class](); + } + } else { + item = {}; + } + resource.previousAttributes[id] = {}; - var injected; - if (_this.utils.isArray(attrs)) { - injected = []; - for (var i = 0; i < attrs.length; i++) { - injected.push(_inject.call(_this, definition, resource, attrs[i])); - } - } else { - if (!(definition.idAttribute in attrs)) { - throw new _this.errors.RuntimeError(errorPrefix + 'attrs: Must contain the property specified by `idAttribute`!'); - } else { - try { - definition.beforeInject(definition.name, attrs); - var id = attrs[definition.idAttribute], - item = this.get(definition.name, id); - - if (!item) { - if (definition.class) { - if (attrs instanceof definition[definition.class]) { - item = attrs; - } else { - item = new definition[definition.class](); - } - } else { - item = {}; - } - resource.previousAttributes[id] = {}; + _this.utils.deepMixIn(item, attrs); + _this.utils.deepMixIn(resource.previousAttributes[id], attrs); - _this.utils.deepMixIn(item, attrs); - _this.utils.deepMixIn(resource.previousAttributes[id], attrs); + resource.collection.push(item); - resource.collection.push(item); + resource.observers[id] = new observe.ObjectObserver(item, _react); + resource.index.put(id, item); - resource.observers[id] = new observe.ObjectObserver(item, _react); - resource.index.put(id, item); + _react({}, {}, {}, function () { + return id; + }); + } else { + _this.utils.deepMixIn(item, attrs); + if (typeof resource.index.touch === 'function') { + resource.index.touch(id); + } else { + resource.index.put(id, resource.index.get(id)); + } + resource.observers[id].deliver(); + } + resource.saved[id] = _this.utils.updateTimestamp(resource.saved[id]); + definition.afterInject(definition.name, item); + injected = item; + } catch (err) { + $log.error(err); + $log.error('inject failed!', definition.name, attrs); + } + } + } + return injected; +} - _react({}, {}, {}, function () { - return id; - }); - } else { - _this.utils.deepMixIn(item, attrs); - if (typeof resource.index.touch === 'function') { - resource.index.touch(id); - } else { - resource.index.put(id, resource.index.get(id)); - } - resource.observers[id].deliver(); - } - resource.saved[id] = _this.utils.updateTimestamp(resource.saved[id]); - definition.afterInject(definition.name, item); - injected = item; - } catch (err) { - $log.error(err); - $log.error('inject failed!', definition.name, attrs); - } - } - } - return injected; +function _injectRelations(definition, injected) { + var _this = this; + _this.utils.forOwn(definition.relations, function (relation, type) { + _this.utils.forOwn(relation, function (def, relationName) { + if (_this.definitions[relationName] && injected[def.localField]) { + try { + injected[def.localField] = _this.inject(relationName, injected[def.localField]); + } catch (err) { + _this.$log.error(errorPrefix + 'Failed to inject ' + type + ' relation: "' + relationName + '"!', err); + } + } + }); + }); } /** @@ -4574,7 +4856,7 @@ function _inject(definition, resource, attrs) { * * - `{IllegalArgumentError}` * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` * * @param {string} resourceName The resource type, e.g. 'user', 'comment', etc. * @param {object|array} attrs The item or collection of items to inject into the data store. @@ -4583,42 +4865,40 @@ function _inject(definition, resource, attrs) { * the items that were injected into the data store. */ function inject(resourceName, attrs, options) { - options = options || {}; - - if (!this.definitions[resourceName]) { - throw new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!'); - } else if (!this.utils.isObject(attrs) && !this.utils.isArray(attrs)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'attrs: Must be an object or an array!', { attrs: { actual: typeof attrs, expected: 'object|array' } }); - } else if (!this.utils.isObject(options)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'options: Must be an object!', { options: { actual: typeof options, expected: 'object' } }); - } - - var definition = this.definitions[resourceName], - resource = this.store[resourceName], - _this = this; - - try { - var injected; - if (!this.$rootScope.$$phase) { - this.$rootScope.$apply(function () { - injected = _inject.apply(_this, [definition, resource, attrs]); - }); - } else { - injected = _inject.apply(_this, [definition, resource, attrs]); - } - return injected; - } catch (err) { - if (!(err instanceof this.errors.RuntimeError)) { - throw new this.errors.UnhandledError(err); - } else { - throw err; - } - } + var IA = this.errors.IA; + + options = options || {}; + + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isObject(attrs) && !this.utils.isArray(attrs)) { + throw new IA(errorPrefix + 'attrs: Must be an object or an array!'); + } else if (!this.utils.isObject(options)) { + throw new IA(errorPrefix + 'options: Must be an object!'); + } + + var definition = this.definitions[resourceName]; + var resource = this.store[resourceName]; + var _this = this; + + var injected; + if (!this.$rootScope.$$phase) { + this.$rootScope.$apply(function () { + injected = _inject.call(_this, definition, resource, attrs); + }); + } else { + injected = _inject.call(_this, definition, resource, attrs); + } + if (definition.relations) { + _injectRelations.call(_this, definition, injected); + } + + return injected; } module.exports = inject; -},{"../../../lib/observe-js/observe-js":1}],55:[function(require,module,exports){ +},{"../../../lib/observe-js/observe-js":1}],56:[function(require,module,exports){ var errorPrefix = 'DS.lastModified(resourceName[, id]): '; /** @@ -4647,8 +4927,7 @@ var errorPrefix = 'DS.lastModified(resourceName[, id]): '; * ## Throws * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` * * @param {string} resourceName The resource type, e.g. 'user', 'comment', etc. * @param {string|number=} id The primary key of the item to remove. @@ -4656,27 +4935,23 @@ var errorPrefix = 'DS.lastModified(resourceName[, id]): '; * `resourceName` with the given primary key was modified. */ function lastModified(resourceName, id) { - if (!this.definitions[resourceName]) { - throw new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!'); - } else if (id && !this.utils.isString(id) && !this.utils.isNumber(id)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'id: Must be a string or a number!', { id: { actual: typeof id, expected: 'string|number' } }); - } - try { - if (id) { - if (!(id in this.store[resourceName].modified)) { - this.store[resourceName].modified[id] = 0; - } - return this.store[resourceName].modified[id]; - } - return this.store[resourceName].collectionModified; - } catch (err) { - throw new this.errors.UnhandledError(err); - } + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (id && !this.utils.isString(id) && !this.utils.isNumber(id)) { + throw new this.errors.IA(errorPrefix + 'id: Must be a string or a number!'); + } + if (id) { + if (!(id in this.store[resourceName].modified)) { + this.store[resourceName].modified[id] = 0; + } + return this.store[resourceName].modified[id]; + } + return this.store[resourceName].collectionModified; } module.exports = lastModified; -},{}],56:[function(require,module,exports){ +},{}],57:[function(require,module,exports){ var errorPrefix = 'DS.lastSaved(resourceName[, id]): '; /** @@ -4712,32 +4987,27 @@ var errorPrefix = 'DS.lastSaved(resourceName[, id]): '; * ## Throws * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` * * @param {string} resourceName The resource type, e.g. 'user', 'comment', etc. * @param {string|number} id The primary key of the item for which to retrieve the lastSaved timestamp. * @returns {number} The timestamp of the last time the item of type `resourceName` with the given primary key was saved. */ function lastSaved(resourceName, id) { - if (!this.definitions[resourceName]) { - throw new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!'); - } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'id: Must be a string or a number!', { id: { actual: typeof id, expected: 'string|number' } }); - } - try { - if (!(id in this.store[resourceName].saved)) { - this.store[resourceName].saved[id] = 0; - } - return this.store[resourceName].saved[id]; - } catch (err) { - throw new this.errors.UnhandledError(err); - } + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { + throw new this.errors.IA(errorPrefix + 'id: Must be a string or a number!'); + } + if (!(id in this.store[resourceName].saved)) { + this.store[resourceName].saved[id] = 0; + } + return this.store[resourceName].saved[id]; } module.exports = lastSaved; -},{}],57:[function(require,module,exports){ +},{}],58:[function(require,module,exports){ var errorPrefix = 'DS.previous(resourceName, id): '; /** @@ -4768,129 +5038,57 @@ var errorPrefix = 'DS.previous(resourceName, id): '; * ## Throws * * - `{IllegalArgumentError}` - * - `{RuntimeError}` - * - `{UnhandledError}` + * - `{NonexistentResourceError}` * * @param {string} resourceName The resource type, e.g. 'user', 'comment', etc. * @param {string|number} id The primary key of the item whose previous attributes are to be retrieved. * @returns {object} The previous attributes of the item of the type specified by `resourceName` with the primary key specified by `id`. */ function previous(resourceName, id) { - if (!this.definitions[resourceName]) { - throw new this.errors.RuntimeError(errorPrefix + resourceName + ' is not a registered resource!'); - } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { - throw new this.errors.IllegalArgumentError(errorPrefix + 'id: Must be a string or a number!', { id: { actual: typeof id, expected: 'string|number' } }); - } - - try { - // return resource from cache - return angular.copy(this.store[resourceName].previousAttributes[id]); - } catch (err) { - throw new this.errors.UnhandledError(err); - } + if (!this.definitions[resourceName]) { + throw new this.errors.NER(errorPrefix + resourceName); + } else if (!this.utils.isString(id) && !this.utils.isNumber(id)) { + throw new this.errors.IA(errorPrefix + 'id: Must be a string or a number!'); + } + + // return resource from cache + return angular.copy(this.store[resourceName].previousAttributes[id]); } module.exports = previous; -},{}],58:[function(require,module,exports){ -/** - * @doc function - * @id errors.types:UnhandledError - * @name UnhandledError - * @description Error that is thrown/returned when Reheat encounters an uncaught/unknown exception. - * @param {Error} error The originally thrown error. - * @returns {UnhandledError} A new instance of `UnhandledError`. - */ -function UnhandledError(error) { - Error.call(this); - if (typeof Error.captureStackTrace === 'function') { - Error.captureStackTrace(this, this.constructor); - } - - error = error || {}; - - /** - * @doc property - * @id errors.types:UnhandledError.type - * @name type - * @propertyOf errors.types:UnhandledError - * @description Name of error type. Default: `"UnhandledError"`. - */ - this.type = this.constructor.name; - - /** - * @doc property - * @id errors.types:UnhandledError.originalError - * @name originalError - * @propertyOf errors.types:UnhandledError - * @description A reference to the original error that was thrown. - */ - this.originalError = error; - - /** - * @doc property - * @id errors.types:UnhandledError.message - * @name message - * @propertyOf errors.types:UnhandledError - * @description Message and stack trace. Same as `UnhandledError#stack`. - */ - this.message = 'UnhandledError: This is an uncaught exception. Please consider submitting an issue at https://github.com/jmdobry/angular-data/issues.\n\n' + - 'Original Uncaught Exception:\n' + (error.stack ? error.stack.toString() : error.stack); - - /** - * @doc property - * @id errors.types:UnhandledError.stack - * @name stack - * @propertyOf errors.types:UnhandledError - * @description Message and stack trace. Same as `UnhandledError#message`. - */ - this.stack = this.message; -} - -UnhandledError.prototype = Object.create(Error.prototype); -UnhandledError.prototype.constructor = UnhandledError; - +},{}],59:[function(require,module,exports){ /** * @doc function * @id errors.types:IllegalArgumentError * @name IllegalArgumentError * @description Error that is thrown/returned when a caller does not honor the pre-conditions of a method/function. * @param {string=} message Error message. Default: `"Illegal Argument!"`. - * @param {object=} errors Object containing information about the error. * @returns {IllegalArgumentError} A new instance of `IllegalArgumentError`. */ -function IllegalArgumentError(message, errors) { - Error.call(this); - if (typeof Error.captureStackTrace === 'function') { - Error.captureStackTrace(this, this.constructor); - } - - /** - * @doc property - * @id errors.types:IllegalArgumentError.type - * @name type - * @propertyOf errors.types:IllegalArgumentError - * @description Name of error type. Default: `"IllegalArgumentError"`. - */ - this.type = this.constructor.name; - - /** - * @doc property - * @id errors.types:IllegalArgumentError.errors - * @name errors - * @propertyOf errors.types:IllegalArgumentError - * @description Object containing information about the error. - */ - this.errors = errors || {}; - - /** - * @doc property - * @id errors.types:IllegalArgumentError.message - * @name message - * @propertyOf errors.types:IllegalArgumentError - * @description Error message. Default: `"Illegal Argument!"`. - */ - this.message = message || 'Illegal Argument!'; +function IllegalArgumentError(message) { + Error.call(this); + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, this.constructor); + } + + /** + * @doc property + * @id errors.types:IllegalArgumentError.type + * @name type + * @propertyOf errors.types:IllegalArgumentError + * @description Name of error type. Default: `"IllegalArgumentError"`. + */ + this.type = this.constructor.name; + + /** + * @doc property + * @id errors.types:IllegalArgumentError.message + * @name message + * @propertyOf errors.types:IllegalArgumentError + * @description Error message. Default: `"Illegal Argument!"`. + */ + this.message = message || 'Illegal Argument!'; } IllegalArgumentError.prototype = Object.create(Error.prototype); @@ -4902,46 +5100,72 @@ IllegalArgumentError.prototype.constructor = IllegalArgumentError; * @name RuntimeError * @description Error that is thrown/returned for invalid state during runtime. * @param {string=} message Error message. Default: `"Runtime Error!"`. - * @param {object=} errors Object containing information about the error. * @returns {RuntimeError} A new instance of `RuntimeError`. */ -function RuntimeError(message, errors) { - Error.call(this); - if (typeof Error.captureStackTrace === 'function') { - Error.captureStackTrace(this, this.constructor); - } - - /** - * @doc property - * @id errors.types:RuntimeError.type - * @name type - * @propertyOf errors.types:RuntimeError - * @description Name of error type. Default: `"RuntimeError"`. - */ - this.type = this.constructor.name; - - /** - * @doc property - * @id errors.types:RuntimeError.errors - * @name errors - * @propertyOf errors.types:RuntimeError - * @description Object containing information about the error. - */ - this.errors = errors || {}; - - /** - * @doc property - * @id errors.types:RuntimeError.message - * @name message - * @propertyOf errors.types:RuntimeError - * @description Error message. Default: `"Runtime Error!"`. - */ - this.message = message || 'RuntimeError Error!'; +function RuntimeError(message) { + Error.call(this); + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, this.constructor); + } + + /** + * @doc property + * @id errors.types:RuntimeError.type + * @name type + * @propertyOf errors.types:RuntimeError + * @description Name of error type. Default: `"RuntimeError"`. + */ + this.type = this.constructor.name; + + /** + * @doc property + * @id errors.types:RuntimeError.message + * @name message + * @propertyOf errors.types:RuntimeError + * @description Error message. Default: `"Runtime Error!"`. + */ + this.message = message || 'RuntimeError Error!'; } RuntimeError.prototype = Object.create(Error.prototype); RuntimeError.prototype.constructor = RuntimeError; +/** + * @doc function + * @id errors.types:NonexistentResourceError + * @name NonexistentResourceError + * @description Error that is thrown/returned when trying to access a resource that does not exist. + * @param {string=} resourceName Name of non-existent resource. + * @returns {NonexistentResourceError} A new instance of `NonexistentResourceError`. + */ +function NonexistentResourceError(resourceName) { + Error.call(this); + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, this.constructor); + } + + /** + * @doc property + * @id errors.types:NonexistentResourceError.type + * @name type + * @propertyOf errors.types:NonexistentResourceError + * @description Name of error type. Default: `"NonexistentResourceError"`. + */ + this.type = this.constructor.name; + + /** + * @doc property + * @id errors.types:NonexistentResourceError.message + * @name message + * @propertyOf errors.types:NonexistentResourceError + * @description Error message. Default: `"Runtime Error!"`. + */ + this.message = (resourceName || '') + ' is not a registered resource!'; +} + +NonexistentResourceError.prototype = Object.create(Error.prototype); +NonexistentResourceError.prototype.constructor = NonexistentResourceError; + /** * @doc interface * @id errors @@ -4949,183 +5173,186 @@ RuntimeError.prototype.constructor = RuntimeError; * @description * Various error types that may be thrown by angular-data. * - * - [UnhandledError](/documentation/api/api/errors.types:UnhandledError) * - [IllegalArgumentError](/documentation/api/api/errors.types:IllegalArgumentError) * - [RuntimeError](/documentation/api/api/errors.types:RuntimeError) + * - [NonexistentResourceError](/documentation/api/api/errors.types:NonexistentResourceError) * * References to the constructor functions of these errors can be found in `DS.errors`. */ module.exports = [function () { - return { - UnhandledError: UnhandledError, - IllegalArgumentError: IllegalArgumentError, - RuntimeError: RuntimeError - }; + return { + IllegalArgumentError: IllegalArgumentError, + IA: IllegalArgumentError, + RuntimeError: RuntimeError, + R: RuntimeError, + NonexistentResourceError: NonexistentResourceError, + NER: NonexistentResourceError + }; }]; -},{}],59:[function(require,module,exports){ +},{}],60:[function(require,module,exports){ (function (window, angular, undefined) { - 'use strict'; + 'use strict'; + + /** + * @doc overview + * @id angular-data + * @name angular-data + * @description + * __Version:__ 0.10.0 + * + * ## Install + * + * #### Bower + * ```text + * bower install angular-data + * ``` + * + * Load `dist/angular-data.js` or `dist/angular-data.min.js` onto your web page after Angular.js. + * + * #### Npm + * ```text + * npm install angular-data + * ``` + * + * Load `dist/angular-data.js` or `dist/angular-data.min.js` onto your web page after Angular.js. + * + * #### Manual download + * Download angular-data-<%= pkg.version %>.js from the [Releases](https://github.com/jmdobry/angular-data/releases) + * section of the angular-data GitHub project. + * + * ## Load into Angular + * Your Angular app must depend on the module `"angular-data.DS"` in order to use angular-data. Loading + * angular-data into your app allows you to inject the following: + * + * - `DS` + * - `DSHttpAdapter` + * - `DSUtils` + * - `DSErrors` + * + * [DS](/documentation/api/api/DS) is the Data Store itself, which you will inject often. + * [DSHttpAdapter](/documentation/api/api/DSHttpAdapter) is useful as a wrapper for `$http` and is configurable. + * [DSUtils](/documentation/api/api/DSUtils) has some useful utility methods. + * [DSErrors](/documentation/api/api/DSErrors) provides references to the various errors thrown by the data store. + */ + angular.module('angular-data.DS', ['ng']) + .factory('DSUtils', require('./utils')) + .factory('DSErrors', require('./errors')) + .provider('DSHttpAdapter', require('./adapters/http')) + .provider('DSLocalStorageAdapter', require('./adapters/localStorage')) + .provider('DS', require('./datastore')) + .config(['$provide', function ($provide) { + $provide.decorator('$q', ['$delegate', function ($delegate) { + // do whatever you you want + $delegate.promisify = function (fn, target) { + var _this = this; + return function () { + var deferred = _this.defer(), + args = Array.prototype.slice.apply(arguments); + + args.push(function (err, result) { + if (err) { + deferred.reject(err); + } else { + deferred.resolve(result); + } + }); - /** - * @doc overview - * @id angular-data - * @name angular-data - * @description - * __Version:__ 0.9.1 - * - * ## Install - * - * #### Bower - * ```text - * bower install angular-data - * ``` - * - * Load `dist/angular-data.js` or `dist/angular-data.min.js` onto your web page after Angular.js. - * - * #### Npm - * ```text - * npm install angular-data - * ``` - * - * Load `dist/angular-data.js` or `dist/angular-data.min.js` onto your web page after Angular.js. - * - * #### Manual download - * Download angular-data-<%= pkg.version %>.js from the [Releases](https://github.com/jmdobry/angular-data/releases) - * section of the angular-data GitHub project. - * - * ## Load into Angular - * Your Angular app must depend on the module `"angular-data.DS"` in order to use angular-data. Loading - * angular-data into your app allows you to inject the following: - * - * - `DS` - * - `DSHttpAdapter` - * - `DSUtils` - * - `DSErrors` - * - * [DS](/documentation/api/api/DS) is the Data Store itself, which you will inject often. - * [DSHttpAdapter](/documentation/api/api/DSHttpAdapter) is useful as a wrapper for `$http` and is configurable. - * [DSUtils](/documentation/api/api/DSUtils) has some useful utility methods. - * [DSErrors](/documentation/api/api/DSErrors) provides references to the various errors thrown by the data store. - */ - angular.module('angular-data.DS', ['ng']) - .factory('DSUtils', require('./utils')) - .factory('DSErrors', require('./errors')) - .provider('DSHttpAdapter', require('./adapters/http')) - .provider('DSLocalStorageAdapter', require('./adapters/localStorage')) - .provider('DS', require('./datastore')) - .config(['$provide', function ($provide) { - $provide.decorator('$q', ['$delegate', function ($delegate) { - // do whatever you you want - $delegate.promisify = function (fn, target) { - var _this = this; - return function () { - var deferred = _this.defer(), - args = Array.prototype.slice.apply(arguments); - - args.push(function (err, result) { - if (err) { - deferred.reject(err); - } else { - deferred.resolve(result); - } - }); - - try { - fn.apply(target || this, args); - } catch (err) { - deferred.reject(err); - } - - return deferred.promise; - }; - }; - return $delegate; - }]); - }]); + try { + fn.apply(target || this, args); + } catch (err) { + deferred.reject(err); + } + + return deferred.promise; + }; + }; + return $delegate; + }]); + }]); })(window, window.angular); -},{"./adapters/http":30,"./adapters/localStorage":31,"./datastore":42,"./errors":58,"./utils":60}],60:[function(require,module,exports){ +},{"./adapters/http":30,"./adapters/localStorage":31,"./datastore":43,"./errors":59,"./utils":61}],61:[function(require,module,exports){ module.exports = [function () { - return { - isString: angular.isString, - isArray: angular.isArray, - isObject: angular.isObject, - isNumber: angular.isNumber, - isFunction: angular.isFunction, - isEmpty: require('mout/lang/isEmpty'), - toJson: angular.toJson, - makePath: require('mout/string/makePath'), - upperCase: require('mout/string/upperCase'), - deepMixIn: require('mout/object/deepMixIn'), - forOwn: require('mout/object/forOwn'), - pick: require('mout/object/pick'), - set: require('mout/object/set'), - contains: require('mout/array/contains'), - filter: require('mout/array/filter'), - toLookup: require('mout/array/toLookup'), - slice: require('mout/array/slice'), - sort: require('mout/array/sort'), - updateTimestamp: function (timestamp) { - var newTimestamp = typeof Date.now === 'function' ? Date.now() : new Date().getTime(); - if (timestamp && newTimestamp <= timestamp) { - return timestamp + 1; - } else { - return newTimestamp; - } - }, - deepFreeze: function deepFreeze(o) { - if (typeof Object.freeze === 'function') { - var prop, propKey; - Object.freeze(o); // First freeze the object. - for (propKey in o) { - prop = o[propKey]; - if (!o.hasOwnProperty(propKey) || typeof prop !== 'object' || Object.isFrozen(prop)) { - // If the object is on the prototype, not an object, or is already frozen, - // skip it. Note that this might leave an unfrozen reference somewhere in the - // object if there is an already frozen object containing an unfrozen object. - continue; - } - - deepFreeze(prop); // Recursively call deepFreeze. - } - } - }, - diffObjectFromOldObject: function (object, oldObject) { - var added = {}; - var removed = {}; - var changed = {}; - - for (var prop in oldObject) { - var newValue = object[prop]; + return { + isString: angular.isString, + isArray: angular.isArray, + isObject: angular.isObject, + isNumber: angular.isNumber, + isFunction: angular.isFunction, + isEmpty: require('mout/lang/isEmpty'), + toJson: angular.toJson, + makePath: require('mout/string/makePath'), + upperCase: require('mout/string/upperCase'), + deepMixIn: require('mout/object/deepMixIn'), + forOwn: require('mout/object/forOwn'), + pick: require('mout/object/pick'), + set: require('mout/object/set'), + contains: require('mout/array/contains'), + filter: require('mout/array/filter'), + toLookup: require('mout/array/toLookup'), + slice: require('mout/array/slice'), + sort: require('mout/array/sort'), + updateTimestamp: function (timestamp) { + var newTimestamp = typeof Date.now === 'function' ? Date.now() : new Date().getTime(); + if (timestamp && newTimestamp <= timestamp) { + return timestamp + 1; + } else { + return newTimestamp; + } + }, + deepFreeze: function deepFreeze(o) { + if (typeof Object.freeze === 'function') { + var prop, propKey; + Object.freeze(o); // First freeze the object. + for (propKey in o) { + prop = o[propKey]; + if (!o.hasOwnProperty(propKey) || typeof prop !== 'object' || Object.isFrozen(prop)) { + // If the object is on the prototype, not an object, or is already frozen, + // skip it. Note that this might leave an unfrozen reference somewhere in the + // object if there is an already frozen object containing an unfrozen object. + continue; + } + + deepFreeze(prop); // Recursively call deepFreeze. + } + } + }, + diffObjectFromOldObject: function (object, oldObject) { + var added = {}; + var removed = {}; + var changed = {}; + + for (var prop in oldObject) { + var newValue = object[prop]; + + if (newValue !== undefined && newValue === oldObject[prop]) + continue; + + if (!(prop in object)) { + removed[prop] = undefined; + continue; + } - if (newValue !== undefined && newValue === oldObject[prop]) - continue; + if (newValue !== oldObject[prop]) + changed[prop] = newValue; + } - if (!(prop in object)) { - removed[prop] = undefined; - continue; - } + for (var prop2 in object) { + if (prop2 in oldObject) + continue; - if (newValue !== oldObject[prop]) - changed[prop] = newValue; - } + added[prop2] = object[prop2]; + } - for (var prop2 in object) { - if (prop2 in oldObject) - continue; - - added[prop2] = object[prop2]; - } - - return { - added: added, - removed: removed, - changed: changed - }; - } - }; + return { + added: added, + removed: removed, + changed: changed + }; + } + }; }]; -},{"mout/array/contains":2,"mout/array/filter":3,"mout/array/slice":7,"mout/array/sort":8,"mout/array/toLookup":9,"mout/lang/isEmpty":14,"mout/object/deepMixIn":21,"mout/object/forOwn":23,"mout/object/pick":26,"mout/object/set":27,"mout/string/makePath":28,"mout/string/upperCase":29}]},{},[59]) \ No newline at end of file +},{"mout/array/contains":2,"mout/array/filter":3,"mout/array/slice":7,"mout/array/sort":8,"mout/array/toLookup":9,"mout/lang/isEmpty":14,"mout/object/deepMixIn":21,"mout/object/forOwn":23,"mout/object/pick":26,"mout/object/set":27,"mout/string/makePath":28,"mout/string/upperCase":29}]},{},[60]) \ No newline at end of file diff --git a/dist/angular-data.min.js b/dist/angular-data.min.js index 410397c..ecb5633 100644 --- a/dist/angular-data.min.js +++ b/dist/angular-data.min.js @@ -1,11 +1,11 @@ /** * @author Jason Dobry * @file angular-data.min.js -* @version 0.9.1 - Homepage +* @version 0.10.0 - Homepage * @copyright (c) 2014 Jason Dobry * @license MIT * * @overview Data store for Angular.js. */ -!function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};b[g][0].call(j.exports,function(a){var c=b[g][1][a];return e(c?c:a)},j,j.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;gb&&a.check();)a.report(),b++}function e(a){for(var b in a)return!1;return!0}function f(a){return e(a.added)&&e(a.removed)&&e(a.changed)}function g(a,b){var c={},d={},e={};for(var f in b){var g=a[f];(void 0===g||g!==b[f])&&(f in a?g!==b[f]&&(e[f]=g):d[f]=void 0)}for(var f in a)f in b||(c[f]=a[f]);return Array.isArray(a)&&a.length!==b.length&&(e.length=a.length),{added:c,removed:d,changed:e}}function h(a,b){var c=b||(Array.isArray(a)?[]:{});for(var d in a)c[d]=a[d];return Array.isArray(a)&&(c.length=a.length),c}function i(a,b,c,d){if(this.closed=!1,this.object=a,this.callback=b,this.target=c,this.token=d,this.reporting=!0,n){var e=this;this.boundInternalCallback=function(a){e.internalCallback(a)}}j(this),this.connect(),this.sync(!0)}function j(a){u&&(t.push(a),i._allObserversCount++)}function k(a,b,c,d){i.call(this,a,b,c,d)}function l(a){this.arr=[],this.callback=a,this.isObserved=!0}function m(a,b,c){for(var d={},e={},f=0;fa&&b.anyChanged);i._allObserversCount=t.length,v=!1}}},u&&(a.Platform.clearObservers=function(){t=[]}),k.prototype=r({__proto__:i.prototype,connect:function(){n&&Object.observe(this.object,this.boundInternalCallback)},sync:function(){n||(this.oldObject=h(this.object))},check:function(a){var b,c;if(n){if(!a)return!1;c={},b=m(this.object,a,c)}else c=this.oldObject,b=g(this.object,this.oldObject);return f(b)?!1:(this.reportArgs=[b.added||{},b.removed||{},b.changed||{}],this.reportArgs.push(function(a){return c[a]}),!0)},disconnect:function(){n?this.object&&Object.unobserve(this.object,this.boundInternalCallback):this.oldObject=void 0}});var x=Object.getPrototypeOf({}),y=Object.getPrototypeOf([]);l.prototype={reset:function(){this.isObserved=!this.isObserved},observe:function(a){if(c(a)&&a!==x&&a!==y){var b=this.arr.indexOf(a);b>=0&&this.arr[b+1]===this.isObserved||(0>b&&(b=this.arr.length,this.arr[b]=a,Object.observe(a,this.callback)),this.arr[b+1]=this.isObserved,this.observe(Object.getPrototypeOf(a)))}},cleanup:function(){for(var a=0,b=0,c=this.isObserved;ba&&(this.arr[a]=d,this.arr[a+1]=c),a+=2):Object.unobserve(d,this.callback),b+=2}this.arr.length=a}};var z={"new":!0,updated:!0,deleted:!0};a.Observer=i,a.Observer.hasObjectObserve=n,a.ObjectObserver=k}((c.Number={isNaN:window.isNaN})?c:c)}).call(this,"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],2:[function(a,b){function c(a,b){return-1!==d(a,b)}var d=a("./indexOf");b.exports=c},{"./indexOf":5}],3:[function(a,b){function c(a,b,c){b=d(b,c);var e=[];if(null==a)return e;for(var f,g=-1,h=a.length;++gc?d+c:c;d>e;){if(a[e]===b)return e;e++}return-1}b.exports=c},{}],6:[function(a,b){function c(a){return null!=a&&""!==a}function d(a,b){return b=b||"",e(a,c).join(b)}var e=a("./filter");b.exports=d},{"./filter":3}],7:[function(a,b){function c(a,b,c){var d=a.length;b=null==b?0:0>b?Math.max(d+b,0):Math.min(b,d),c=null==c?d:0>c?Math.max(d+c,0):Math.min(c,d);for(var e=[];c>b;)e.push(a[b++]);return e}b.exports=c},{}],8:[function(a,b){function c(a,b){if(null==a)return[];if(a.length<2)return a;null==b&&(b=d);var f,g,h;return f=~~(a.length/2),g=c(a.slice(0,f),b),h=c(a.slice(f,a.length),b),e(g,h,b)}function d(a,b){return b>a?-1:a>b?1:0}function e(a,b,c){for(var d=[];a.length&&b.length;)d.push(c(a[0],b[0])<=0?a.shift():b.shift());return a.length&&d.push.apply(d,a),b.length&&d.push.apply(d,b),d}b.exports=c},{}],9:[function(a,b){function c(a,b){var c={};if(null==a)return c;var e,f=-1,g=a.length;if(d(b))for(;++f"in a?d=d&&c[b]>a[">"]:">="in a?d=d&&c[b]>=a[">="]:"<"in a?d=d&&c[b]e?-1:e>d?1:0:e>d?-1:d>e?1:0})}}return this.utils.isNumber(b.query.limit)&&this.utils.isNumber(b.query.skip)?i=this.utils.slice(i,b.query.skip,Math.min(i.length,b.query.skip+b.query.limit)):this.utils.isNumber(b.query.limit)?i=this.utils.slice(i,0,Math.min(i.length,b.query.limit)):this.utils.isNumber(b.query.skip)&&(i=b.query.skip=b?a+1:b},deepFreeze:function b(a){if("function"==typeof Object.freeze){var c,d;Object.freeze(a);for(d in a)c=a[d],a.hasOwnProperty(d)&&"object"==typeof c&&!Object.isFrozen(c)&&b(c)}},diffObjectFromOldObject:function(a,b){var c={},d={},e={};for(var f in b){var g=a[f];(void 0===g||g!==b[f])&&(f in a?g!==b[f]&&(e[f]=g):d[f]=void 0)}for(var h in a)h in b||(c[h]=a[h]);return{added:c,removed:d,changed:e}}}}]},{"mout/array/contains":2,"mout/array/filter":3,"mout/array/slice":7,"mout/array/sort":8,"mout/array/toLookup":9,"mout/lang/isEmpty":14,"mout/object/deepMixIn":21,"mout/object/forOwn":23,"mout/object/pick":26,"mout/object/set":27,"mout/string/makePath":28,"mout/string/upperCase":29}]},{},[59]); \ No newline at end of file +!function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};b[g][0].call(j.exports,function(a){var c=b[g][1][a];return e(c?c:a)},j,j.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;gb&&a.check();)a.report(),b++}function e(a){for(var b in a)return!1;return!0}function f(a){return e(a.added)&&e(a.removed)&&e(a.changed)}function g(a,b){var c={},d={},e={};for(var f in b){var g=a[f];(void 0===g||g!==b[f])&&(f in a?g!==b[f]&&(e[f]=g):d[f]=void 0)}for(var f in a)f in b||(c[f]=a[f]);return Array.isArray(a)&&a.length!==b.length&&(e.length=a.length),{added:c,removed:d,changed:e}}function h(a,b){var c=b||(Array.isArray(a)?[]:{});for(var d in a)c[d]=a[d];return Array.isArray(a)&&(c.length=a.length),c}function i(a,b,c,d){if(this.closed=!1,this.object=a,this.callback=b,this.target=c,this.token=d,this.reporting=!0,n){var e=this;this.boundInternalCallback=function(a){e.internalCallback(a)}}j(this),this.connect(),this.sync(!0)}function j(a){u&&(t.push(a),i._allObserversCount++)}function k(a,b,c,d){i.call(this,a,b,c,d)}function l(a){this.arr=[],this.callback=a,this.isObserved=!0}function m(a,b,c){for(var d={},e={},f=0;fa&&b.anyChanged);i._allObserversCount=t.length,v=!1}}},u&&(a.Platform.clearObservers=function(){t=[]}),k.prototype=r({__proto__:i.prototype,connect:function(){n&&Object.observe(this.object,this.boundInternalCallback)},sync:function(){n||(this.oldObject=h(this.object))},check:function(a){var b,c;if(n){if(!a)return!1;c={},b=m(this.object,a,c)}else c=this.oldObject,b=g(this.object,this.oldObject);return f(b)?!1:(this.reportArgs=[b.added||{},b.removed||{},b.changed||{}],this.reportArgs.push(function(a){return c[a]}),!0)},disconnect:function(){n?this.object&&Object.unobserve(this.object,this.boundInternalCallback):this.oldObject=void 0}});var x=Object.getPrototypeOf({}),y=Object.getPrototypeOf([]);l.prototype={reset:function(){this.isObserved=!this.isObserved},observe:function(a){if(c(a)&&a!==x&&a!==y){var b=this.arr.indexOf(a);b>=0&&this.arr[b+1]===this.isObserved||(0>b&&(b=this.arr.length,this.arr[b]=a,Object.observe(a,this.callback)),this.arr[b+1]=this.isObserved,this.observe(Object.getPrototypeOf(a)))}},cleanup:function(){for(var a=0,b=0,c=this.isObserved;ba&&(this.arr[a]=d,this.arr[a+1]=c),a+=2):Object.unobserve(d,this.callback),b+=2}this.arr.length=a}};var z={"new":!0,updated:!0,deleted:!0};a.Observer=i,a.Observer.hasObjectObserve=n,a.ObjectObserver=k}((c.Number={isNaN:window.isNaN})?c:c)}).call(this,"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],2:[function(a,b){function c(a,b){return-1!==d(a,b)}var d=a("./indexOf");b.exports=c},{"./indexOf":5}],3:[function(a,b){function c(a,b,c){b=d(b,c);var e=[];if(null==a)return e;for(var f,g=-1,h=a.length;++gc?d+c:c;d>e;){if(a[e]===b)return e;e++}return-1}b.exports=c},{}],6:[function(a,b){function c(a){return null!=a&&""!==a}function d(a,b){return b=b||"",e(a,c).join(b)}var e=a("./filter");b.exports=d},{"./filter":3}],7:[function(a,b){function c(a,b,c){var d=a.length;b=null==b?0:0>b?Math.max(d+b,0):Math.min(b,d),c=null==c?d:0>c?Math.max(d+c,0):Math.min(c,d);for(var e=[];c>b;)e.push(a[b++]);return e}b.exports=c},{}],8:[function(a,b){function c(a,b){if(null==a)return[];if(a.length<2)return a;null==b&&(b=d);var f,g,h;return f=~~(a.length/2),g=c(a.slice(0,f),b),h=c(a.slice(f,a.length),b),e(g,h,b)}function d(a,b){return b>a?-1:a>b?1:0}function e(a,b,c){for(var d=[];a.length&&b.length;)d.push(c(a[0],b[0])<=0?a.shift():b.shift());return a.length&&d.push.apply(d,a),b.length&&d.push.apply(d,b),d}b.exports=c},{}],9:[function(a,b){function c(a,b){var c={};if(null==a)return c;var e,f=-1,g=a.length;if(d(b))for(;++f"===g?c=b?a[f]>d:c&&a[f]>d:">="===g?c=b?a[f]>=d:c&&a[f]>=d:"<"===g?c=b?a[f]"===g?c=b?a[f]>d:c||a[f]>d:"|>="===g?c=b?a[f]>=d:c||a[f]>=d:"|<"===g?c=b?a[f]f?-1:f>d?1:0:f>d?-1:d>f?1:0})});var j=angular.isNumber(c.limit)?c.limit:null,k=null;return angular.isNumber(c.skip)?k=c.skip:angular.isNumber(c.offset)&&(k=c.offset),j&&k?f=this.utils.slice(f,k,Math.min(f.length,k+j)):this.utils.isNumber(j)?f=this.utils.slice(f,0,Math.min(f.length,j)):this.utils.isNumber(k)&&(f=k=b?a+1:b},deepFreeze:function b(a){if("function"==typeof Object.freeze){var c,d;Object.freeze(a);for(d in a)c=a[d],a.hasOwnProperty(d)&&"object"==typeof c&&!Object.isFrozen(c)&&b(c)}},diffObjectFromOldObject:function(a,b){var c={},d={},e={};for(var f in b){var g=a[f];(void 0===g||g!==b[f])&&(f in a?g!==b[f]&&(e[f]=g):d[f]=void 0)}for(var h in a)h in b||(c[h]=a[h]);return{added:c,removed:d,changed:e}}}}]},{"mout/array/contains":2,"mout/array/filter":3,"mout/array/slice":7,"mout/array/sort":8,"mout/array/toLookup":9,"mout/lang/isEmpty":14,"mout/object/deepMixIn":21,"mout/object/forOwn":23,"mout/object/pick":26,"mout/object/set":27,"mout/string/makePath":28,"mout/string/upperCase":29}]},{},[60]); \ No newline at end of file diff --git a/guide/angular-cache/basics.doc b/guide/angular-cache/basics.md similarity index 100% rename from guide/angular-cache/basics.doc rename to guide/angular-cache/basics.md diff --git a/guide/angular-cache/configure.doc b/guide/angular-cache/configure.md similarity index 100% rename from guide/angular-cache/configure.doc rename to guide/angular-cache/configure.md diff --git a/guide/angular-cache/http.doc b/guide/angular-cache/http.md similarity index 100% rename from guide/angular-cache/http.doc rename to guide/angular-cache/http.md diff --git a/guide/angular-cache/index.doc b/guide/angular-cache/index.md similarity index 100% rename from guide/angular-cache/index.doc rename to guide/angular-cache/index.md diff --git a/guide/angular-cache/storage.doc b/guide/angular-cache/storage.md similarity index 100% rename from guide/angular-cache/storage.doc rename to guide/angular-cache/storage.md diff --git a/guide/angular-data-mocks/index.doc b/guide/angular-data-mocks/index.md similarity index 100% rename from guide/angular-data-mocks/index.doc rename to guide/angular-data-mocks/index.md diff --git a/guide/angular-data-mocks/overview.doc b/guide/angular-data-mocks/overview.md similarity index 100% rename from guide/angular-data-mocks/overview.doc rename to guide/angular-data-mocks/overview.md diff --git a/guide/angular-data-mocks/setup.doc b/guide/angular-data-mocks/setup.md similarity index 100% rename from guide/angular-data-mocks/setup.doc rename to guide/angular-data-mocks/setup.md diff --git a/guide/angular-data-mocks/testing.doc b/guide/angular-data-mocks/testing.md similarity index 100% rename from guide/angular-data-mocks/testing.doc rename to guide/angular-data-mocks/testing.md diff --git a/guide/angular-data/adapters.doc b/guide/angular-data/adapters.md similarity index 100% rename from guide/angular-data/adapters.doc rename to guide/angular-data/adapters.md diff --git a/guide/angular-data/asynchronous.doc b/guide/angular-data/asynchronous.md similarity index 100% rename from guide/angular-data/asynchronous.doc rename to guide/angular-data/asynchronous.md diff --git a/guide/angular-data/how.doc b/guide/angular-data/how.md similarity index 100% rename from guide/angular-data/how.doc rename to guide/angular-data/how.md diff --git a/guide/angular-data/index.doc b/guide/angular-data/index.md similarity index 100% rename from guide/angular-data/index.doc rename to guide/angular-data/index.md diff --git a/guide/angular-data/overview.doc b/guide/angular-data/overview.md similarity index 72% rename from guide/angular-data/overview.doc rename to guide/angular-data/overview.md index eac9149..f715426 100644 --- a/guide/angular-data/overview.doc +++ b/guide/angular-data/overview.md @@ -14,38 +14,36 @@ Angular-data is an in-browser data store for [Angular.js](http://angularjs.org). ## Example: ```js angular.module('myApp', ['angular-data.DS']) - .config(function (DSProvider) { - DSProvider.defaults.baseUrl = '/api'; - }) - .run(function (DS) { - DS.defineResource({ - name: 'post', - endpoint: 'posts', - idAttribute: '_id' - }); - }) - .controller('PostCtrl', function ($scope, DS) { - var params = { - query: { - where: { - author: { - '==': 'John Anderson' - } - } - } - }; + .config(function (DSProvider) { + DSProvider.defaults.baseUrl = '/api'; + }) + .run(function (DS) { + DS.defineResource({ + name: 'post', + endpoint: 'posts', + idAttribute: '_id' + }); + }) + .controller('PostCtrl', function ($scope, DS) { + var params = { + where: { + author: { + '==': 'John Anderson' + } + } + }; - DS.findAll('post', params); + DS.findAll('post', params); - DS.bindAll($scope', 'posts', 'post', params); + DS.bindAll($scope', 'posts', 'post', params); - // Verbose way of doing the bindAll() above, but gives more control - $scope.$watch(function () { - return DS.lastModified('post'); - }, function () { - $scope.posts = DS.filter('post', query); - }); - }); + // Verbose way of doing the bindAll() above, but gives more control + $scope.$watch(function () { + return DS.lastModified('post'); + }, function () { + $scope.posts = DS.filter('post', params); + }); + }); ``` Angular-data does not decorate your data in any way–your data maintains the pure POJO representation common to Angular diff --git a/guide/angular-data/queries.doc b/guide/angular-data/queries.doc deleted file mode 100644 index 420e8b0..0000000 --- a/guide/angular-data/queries.doc +++ /dev/null @@ -1,130 +0,0 @@ -@doc overview -@id queries -@name Queries and Filtering -@description - -Items injected into the data store are indexed by their primary key, but they're also stored in collections. - -`DS.findAll()` is used to asynchronously query a persistence layer for a collection of items. `DS.filter()` is used to -synchronously query the items already in the data store. Here are some examples: - -Translates into a `GET` request to `/post`: -```js -DS.findAll('post', {}).then(function (posts) { - posts; // [ { ... }, { ... }, ... , { ... } ] - - DS.filter('post', {}); // returns all posts in the data store -}).catch(function (err) { - err; // reason why query failed -}); -``` - -Translates into a `GET` request to `/post?query={"where":{"author":{"==":"John Anderson"}}}'` (url encoded of course): -```js -var params = { - query: { - where: { - author: { - '==': 'John Anderson' - } - } - } -}; - -DS.findAll('post', params).then(function (posts) { - posts; // [ { ... }, { ... }, ... , { ... } ] - - DS.filter('post', params); // filters the posts already in the data store -}).catch(function (err) { - err; // reason why query failed -}); -``` - -Angular-data has a "query" language that it natively understands. To sync up how your API and the data store query -collections you will need to do one of the following: - -##### Option A -Configure your API to understand angular-data's query syntax, translating the query object into the appropriate database query. - -##### Option B -Replace Angular-data's filter with your own filter that works the same way your API already works. - -Here's is angular-data's filter (it's pretty simple): - -```js -function (resourceName, where, attrs) { - var keep = true; - utils.forOwn(where, function (clause, field) { - if (utils.isString(clause)) { - clause = { - '===': clause - }; - } else if (utils.isNumber(clause)) { - clause = { - '==': clause - }; - } - if ('==' in clause) { - keep = keep && (attrs[field] == clause['==']); - } else if ('===' in clause) { - keep = keep && (attrs[field] === clause['===']); - } else if ('!=' in clause) { - keep = keep && (attrs[field] != clause['!=']); - } else if ('>' in clause) { - keep = keep && (attrs[field] > clause['>']); - } else if ('>=' in clause) { - keep = keep && (attrs[field] >= clause['>=']); - } else if ('<' in clause) { - keep = keep && (attrs[field] < clause['<']); - } else if ('<=' in clause) { - keep = keep && (attrs[field] <= clause['<=']); - } else if ('in' in clause) { - keep = keep && utils.contains(clause['in'], attrs[field]); - } - }); - return keep; -} -``` - -If the following `params` argument is passed to `DS.filter`: - -```js -var params = { - query: { - where: { - author: { - '==': 'John Anderson' - } - } - } -}; - -DS.filter('post', params); -``` - -Then `DS.filter` will return the posts (already in the data store) where `post.author == 'John Anderson'` is true. - -Here's how to replace Angular-data's filter: - -```js -DSProvider.defaults.filter = function (resourceName, where, attrs) { - // custom filter - // this will be called on every item in the collection - // return true to keep `attrs` in the result set - // return false to exclude `attrs` from the result set -}); -``` - -You can even override the filter per-resource: - -```js -DS.defineResource({ - name: 'post', - filter: function (resourceName, where, attrs) { - // custom filter for posts - // this will be called on every item in the collection - // return true to keep `attrs` in the result set - // return false to exclude `attrs` from the result set - } -}); -``` diff --git a/guide/angular-data/queries.md b/guide/angular-data/queries.md new file mode 100644 index 0000000..ea7fc97 --- /dev/null +++ b/guide/angular-data/queries.md @@ -0,0 +1,256 @@ +@doc overview +@id queries +@name Queries and Filtering +@description + +Items injected into the data store are indexed by their primary key, but they're also stored in collections. + +`DS.findAll()` is used to asynchronously query a persistence layer for a collection of items. `DS.filter()` is used to +synchronously query the items already in the data store. Here are some examples: + +Translates into a `GET` request to `/post`: +```js +DS.findAll('post', {}).then(function (posts) { + posts; // [ { ... }, { ... }, ... , { ... } ] + + DS.filter('post', {}); // returns all posts in the data store +}).catch(function (err) { + err; // reason why query failed +}); +``` + +Translates into a `GET` request to `/post?where={"author":{"==":"John Anderson"}}'` (url encoded of course): +```js +var params = { + where: { + author: { + '==': 'John Anderson' + } + } +}; + +DS.findAll('post', params).then(function (posts) { + posts; // [ { ... }, { ... }, ... , { ... } ] + + DS.filter('post', params); // filters the posts already in the data store +}).catch(function (err) { + err; // reason why query failed +}); +``` + +Angular-data has a "query" language that it natively understands. To sync up how your API and the data store query +collections you will need to do one of the following: + +##### Option A +Configure your API to understand angular-data's query syntax, translating the query object into the appropriate database query. + +##### Option B +Replace Angular-data's filter with your own filter that works the same way your API already works. + +If the following `params` argument is passed to `DS.filter`: + +```js +var params = { + where: { + author: { + '==': 'John Anderson' + } + } +}; + +DS.filter('post', params); +``` + +Then `DS.filter` will return the posts (already in the data store) where `post.author == 'John Anderson'`. + +Here's how to replace Angular-data's filter: + +```js +// override how DS.filter handles the "where", "skip", "limit" and "orderBy" clauses +DSProvider.defaults.filter = function (collection, resourceName, params, options) { + // examine params and + // decide whether to exclude items, skip items, start from an offset, or sort the items + + // see the [default implementation that ships with angular-data](https://github.com/jmdobry/angular-data/blob/master/src/datastore/index.js#L12) + // overriding this method is useful when our server only understands a certain + // params format and you want angular-data's filter to behave the same as your server + + // angular-data looks for the following fields: + // - where + // - skip (or offset) + // - limit + // - orderBy (or sort) + + // return the filtered collection +}; +``` + +You can even override the filter per-resource: + +```js +DS.defineResource({ + name: 'post', + filter: function (collection, resourceName, params, options) { + // … + } +}); +``` + +#### angular-data's default filter + +Params definition: +```js +{ + where: { + : { + : , + : , + : + }, + : { + : , + : , + : + }, + : { + : , + : , + : + } + }, + skip: , + offset: , // same as offset + limit: , + orderBy: , + sort: // same as orderBy +} +``` + +##### where +Where can be any top-level field on the item being filtered and is one of the operators angular-data's default filter understands. + +__Operators__: + +These get ANDed into the final result: +- `==` +- `===` +- `!=` +- `!==` +- `>` +- `>=` +- `<` +- `<=` +- `in` + +These get ORed into the final result: +- `|==` +- `|===` +- `|!=` +- `|!==` +- `|>` +- `|>=` +- `|<` +- `|<=` +- `|in` + +##### skip or offset +Should be a number + +##### limit +Should be a number + +##### orderBy or sort +Should be an array of arrays. + +#### Examples: + +```js +DS.filter('post', { + where: { + author: { + '==': 'John Anderson' + } + }, + skip: 20, + limit: 100 +}); +``` + +```js +DS.filter('post', { + orderBy: [ + ['author', 'DESC'], + ['created_date', 'DESC'] + ] +}); +``` + +```js +DS.filter('post', { + where: { + author: { + 'in': ['John', 'Sally'] + } + } +}); +``` + +`age` is greater than or equal to 15 AND less than or equal to 30. +```js +DS.filter('user', { + where: { + age: { + '>=': 15, + '<=': 30 + } + } +}); +``` + +`age` is less than 15 OR greater than 30. +```js +DS.filter('post', { + where: { + age: { + '|<': 15, + '|>': 30 + } + } +}); +``` + +#### Shortcuts + +These are equivalent: +```js +DS.filter('post', { + author: 'John' +}); +DS.filter('post', { + where: { + author: 'John' + } +}); +DS.filter('post', { + where: { + author: { + 'in': ['John', 'Sally'] + } + } +}); +``` + +These are equivalent: +```js +DS.filter('post', { + orderBy: 'age' +}); +DS.filter('post', { + orderBy: ['age', 'ASC'] +}); +DS.filter('post', { + orderBy: [ + ['age', 'ASC'] + ] +}); +``` diff --git a/guide/angular-data/resource/resource.doc b/guide/angular-data/resource/resource.md similarity index 57% rename from guide/angular-data/resource/resource.doc rename to guide/angular-data/resource/resource.md index 433f4f8..3ec2596 100644 --- a/guide/angular-data/resource/resource.doc +++ b/guide/angular-data/resource/resource.md @@ -211,3 +211,237 @@ user.fullName(); // "John Anderson" user.constructor; // function User() { ... } ``` + +@doc overview +@id relations +@name Defining Relations +@description + +Angular-data supports relations to some extent. If `GET /user/10` returns: + +```js +{ + id: 10, + name: 'John Anderson', + profile: { + email: 'John Anderson', + id: 18, + userId: 10 + } +} +``` + +then only the user object is injected into the store. If you've defined the `hasOne` relationship of users to profiles, +not only will the user be injected into the store, but the profile as well. Example: + +Without defining relations: +```js +var userJson = { + id: 10, + name: 'John Anderson', + profile: { + email: 'John Anderson', + id: 18, + userId: 10 + } +}; + +DS.inject('user', userJson); + +assert.deepEqual(DS.get('user', 10), userJson); +assert.isUndefined(DS.get('profile', 18)); +``` + +With relations defined: +```js +var userJson = { + id: 10, + name: 'John Anderson', + profile: { + email: 'John Anderson', + id: 18, + userId: 10 + } +}; + +DS.inject('user', userJson); + +assert.deepEqual(DS.get('user', 10), userJson); +assert.deepEqual(DS.get('profile', 18), userJson.profile); +assert.deepEqual(DS.get('profile', 18), DS.get('user', 10).profile); +``` + +## Defining relations: +```js +DS.defineResource({ + name: 'user', + relations: { + hasMany: { + comment: { + localField: 'comments', + foreignKey: 'userId' + } + }, + hasOne: { + profile: { + localField: 'profile', + foreignKey: 'userId' + } + }, + belongsTo: { + organization: { + localKey: 'organizationId', + localField: 'organization' + } + } + } +}); + +DS.defineResource({ + name: 'organization', + relations: { + hasMany: { + user: { + localField: 'users', + foreignKey: 'organizationId' + } + } + } +}); + +DS.defineResource({ + name: 'profile', + relations: { + belongsTo: { + user: { + localField: 'user', + localKey: 'userId' + } + } + } +}); + +DS.defineResource({ + name: 'comment', + relations: { + belongsTo: { + user: { + localField: 'user', + localKey: 'userId' + } + } + } +}); +``` + +`DS.inject` can be called by you, and is also used internally by `find`, `findAll`, `create`, `update`, `updateAll`, `save` and `refresh`. + +## Loading relations + +#### Lazy +```js +DS.find('user', 10).then(function (user) { + // let's assume the server only returned the user + user.comments; // undefined + user.profile; // undefined + + DS.loadRelations('user', user, ['comment', 'profile']).then(function (user) { + user.comments; // array + user.profile; // object + }); +}); +``` + +#### Direct (requires server-side support) + +When you call `DS.find` you can pass `params` to the http adapter like so: +```js +DS.find('user', 10, { + // will be serialized to the query string + params: {...} +}); +``` + +For `findAll` it works like this: +```js +var params = {...}; // will be serialized to the query string +DS.findAll('user', params); +``` + +You could, for example, configure your server to look for a query string parameter called `with` or something, which tells +the server which relations to include with the response, for example: + +```js +DS.find('user', 10, { + params: { + with: ['comment', 'organization'] + } +}); +``` + +With default settings (and using the http adapter) this will produce `GET /user/10with=comment&with=organization`. + +You can configure your server to also return the `comment` and `organization` relations in the response: + +```js +{ + id: 10, + name: 'John Anderson', + organizationId: 15, + comments: [...], + organization: {...} +} +``` + +If you've told angular-data about the relations, then the comments and organization will be injected into the data store in addition to the user. + +@doc overview +@id computed +@name Computed Properties +@description + +Angular-data supports computed properties. When you define a computed property you also define the fields that it depends on. +The computed property will only be updated when one of those fields changes. + +## Example +```js +DS.defineResource('user', { + computed: { + // each function's argument list defines the fields + // that the computed property depends on + fullName: function (first, last) { + return first + ' ' + last; + } + } +}); + +var user = DS.inject('user', { + id: 1, + first: 'John', + last: 'Anderson' +}); + +user.fullName; // "John Anderson" + +user.first = 'Fred'; + +// angular-data relies on dirty-checking, so the +// computed property hasn't been updated yet +user.fullName; // "John Anderson" + +DS.digest(); + +user.fullName; // "Fred Anderson" + +user.first = 'George'; + +$timeout(function () { + user.fullName; // "George Anderson" +}); + +user.first = 'William'; + +$scope.$apply(function () { + user.fullName; // "William Anderson" +}); +``` diff --git a/guide/angular-data/resources.doc b/guide/angular-data/resources.md similarity index 100% rename from guide/angular-data/resources.doc rename to guide/angular-data/resources.md diff --git a/guide/angular-data/synchronous.doc b/guide/angular-data/synchronous.md similarity index 100% rename from guide/angular-data/synchronous.doc rename to guide/angular-data/synchronous.md diff --git a/guide/api/api.doc b/guide/api/api.md similarity index 100% rename from guide/api/api.doc rename to guide/api/api.md diff --git a/guide/nav.html b/guide/nav.html index 2593d85..f4fc5d3 100644 --- a/guide/nav.html +++ b/guide/nav.html @@ -72,7 +72,7 @@ API