├── .gitignore ├── bower.json ├── .travis.yml ├── .editorconfig ├── package.json ├── gulpfile.js ├── LICENSE ├── README.md ├── src └── angular-mongolab.js └── test └── angular-mongolab.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angularjs-mongolab", 3 | "devDependencies": { 4 | "angular-mocks": "1.2.22" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | before_script: 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | - npm install -g gulp && npm install -g bower 9 | 10 | script: bower install && gulp test -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 4 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "https://github.com/pkozlowski-opensource/angularjs-mongolab-promise/graphs/contributors", 3 | "name": "angularjs-mongolab", 4 | "version": "1.0.0-RC1", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/pkozlowski-opensource/angularjs-mongolab-promise.git" 8 | }, 9 | "devDependencies": { 10 | "gulp": "~3.8.6", 11 | "karma": "~0.12.9", 12 | "karma-jasmine": "~0.1.5", 13 | "karma-chrome-launcher": "~0.1.3", 14 | "karma-firefox-launcher": "~0.1.3", 15 | "gulp-jshint": "~1.8.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var jshint = require('gulp-jshint'); 3 | var karma = require('karma').server; 4 | 5 | var PATHS = { 6 | src: 'src/**/*.js', 7 | test: 'test/**/*.spec.js' 8 | }; 9 | 10 | 11 | var karmaCommonConf = { 12 | browsers: process.env.TRAVIS ? ['Firefox'] : ['Chrome'], 13 | frameworks: ['jasmine'], 14 | files: [ 15 | 'bower_components/angular/angular.js', 16 | 'bower_components/angular-mocks/angular-mocks.js', 17 | PATHS.src, PATHS.test 18 | ], 19 | singleRun: true, 20 | reporters: ['dots'] 21 | }; 22 | 23 | gulp.task('lint', function () { 24 | gulp.src([PATHS.src, PATHS.test]) 25 | .pipe(jshint()) 26 | .pipe(jshint.reporter('default')) 27 | .pipe(jshint.reporter('fail')); 28 | }); 29 | 30 | gulp.task('tdd', function (done) { 31 | karmaCommonConf.singleRun = false; 32 | karma.start(karmaCommonConf, done); 33 | }); 34 | 35 | gulp.task('test', function (done) { 36 | karma.start(karmaCommonConf, done); 37 | }); 38 | 39 | gulp.task('default', ['lint', 'test']); 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2012-2014 Pawel Kozlowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/pkozlowski-opensource/angularjs-mongolab.png)](http://travis-ci.org/pkozlowski-opensource/angularjs-mongolab) 2 | [![devDependency Status](https://david-dm.org/pkozlowski-opensource/angularjs-mongolab/dev-status.png?branch=master)](https://david-dm.org/pkozlowski-opensource/angularjs-mongolab#info=devDependencies) 3 | 4 | ## Promise-aware [MongoLab](https://mongolab.com/home) $resource-like adapter for [AngularJS](http://angularjs.org/) 5 | 6 | ### Introduction 7 | 8 | This repository hosts a Mongolab [$resource](http://docs.angularjs.org/api/ngResource.$resource)-like adapter for [AngularJS](http://angularjs.org/). 9 | It is based on [$http](http://docs.angularjs.org/api/ng.$http) and is working with [promises](https://docs.angularjs.org/api/ng/service/$q). 10 | 11 | This is a small wrapper around the AngularJS $http that makes setting up and working with MongoLab easy. It has an interface very similar to $resource but works with promises. 12 | It significantly reduces the amount of boilerplate code one needs to write when interacting with MongoDB / MongoLab (especially around URLs handling, resource objects creation and identifiers handling). 13 | 14 | ### Examples 15 | To see it in action check this plunker: (http://plnkr.co/edit/Bb8GSA?p=preview). 16 | 17 | ### Usage instructions 18 | 19 | Firstly you need to include both AngularJS and the `angular-mongolab.js` script [https://github.com/pkozlowski-opensource/angularjs-mongolab/blob/master/src/angular-mongolab.js](from this repository). 20 | 21 | Then, you need to configure 2 parameters: 22 | * MongoLab key (`API_KEY`) 23 | * database name (`DB_NAME`) 24 | 25 | Configuration parameters needs to be specified in a constant `MONGOLAB_CONFIG` on an application's module: 26 | ```JavaScript 27 | var app = angular.module('app', ['mongolabResourceHttp']); 28 | 29 | app.constant('MONGOLAB_CONFIG',{API_KEY:'your key goes here', DB_NAME:'angularjs'}); 30 | ``` 31 | Then, creating new resources is very, very easy and boils down to calling `$mongolabResource` with a MongoDB collection name: 32 | ```JavaScript 33 | app.factory('Project', function ($mongolabResourceHttp) { 34 | return $mongolabResourceHttp('projects'); 35 | }); 36 | ``` 37 | As soon as the above is done you are ready to inject and use a freshly created resource in your services and controllers: 38 | ```JavaScript 39 | app.controller('AppController', function ($scope, Project) { 40 | Project.all().then(function(projects){ 41 | $scope.projects = projects; 42 | }); 43 | }); 44 | ``` 45 | 46 | ### Documentation 47 | 48 | Since this $resource-like implementation is based on `$http` and returns a promise. 49 | Each resource created with the `$mongolabResourceHttp` will be equipped with the following methods: 50 | * on the class level: 51 | * `Resource.all([options])` 52 | * `Resource.query(criteriaObject,[options])` 53 | * `Resource.getById(idString)` 54 | * `Resource.getByIds(idsArray)` 55 | * `Resource.count(criteriaObject)` 56 | * `Resource.distinct(fieldName, criteriaObject)` 57 | * on an instance level: 58 | * `resource.$id()` 59 | * `resource.$save()` 60 | * `resource.$update()` 61 | * `resource.$saveOrUpdate()` 62 | * `resource.$remove()` 63 | 64 | Resource `all` and `query` supported options: 65 | * `sort`: ex.: `Resource.all({ sort: {priority: 1} });` 66 | * `limit`: ex.: `Resource.all({ limit: 10 });` 67 | * `fields`: `1` - includes a field, `0` - excludes a field, ex.: `Resource.all({ fields: {name: 1, notes: 0} });` 68 | * `skip`: ex.: `Resource.all({ skip: 10 });` 69 | 70 | ### Contributting 71 | 72 | New contributions are always welcomed. Just open a pull request making sure that it contains tests, doc updates. 73 | Checked if the [Travis-CI build](https://travis-ci.org/pkozlowski-opensource/angularjs-mongolab) is alright. 74 | 75 | ### Contributtors 76 | 77 | https://github.com/pkozlowski-opensource/angularjs-mongolab/graphs/contributors 78 | -------------------------------------------------------------------------------- /src/angular-mongolab.js: -------------------------------------------------------------------------------- 1 | angular.module('mongolabResourceHttp', []) 2 | .factory('$mongolabResourceHttp', ['MONGOLAB_CONFIG', '$http', '$q', function (MONGOLAB_CONFIG, $http, $q) { 3 | 4 | function MmongolabResourceFactory(collectionName) { 5 | 6 | var config = angular.extend({ 7 | BASE_URL: 'https://api.mongolab.com/api/1/databases/' 8 | }, MONGOLAB_CONFIG); 9 | 10 | var dbUrl = config.BASE_URL + config.DB_NAME; 11 | var collectionUrl = dbUrl + '/collections/' + collectionName; 12 | var defaultParams = {apiKey: config.API_KEY}; 13 | 14 | var resourceRespTransform = function (response) { 15 | return new Resource(response.data); 16 | }; 17 | 18 | var resourcesArrayRespTransform = function (response) { 19 | return response.data.map(function(item){ 20 | return new Resource(item); 21 | }); 22 | }; 23 | 24 | var preparyQueryParam = function (queryJson) { 25 | return angular.isObject(queryJson) && Object.keys(queryJson).length ? {q: JSON.stringify(queryJson)} : {}; 26 | }; 27 | 28 | var Resource = function (data) { 29 | angular.extend(this, data); 30 | }; 31 | 32 | Resource.query = function (queryJson, options) { 33 | 34 | var prepareOptions = function (options) { 35 | 36 | var optionsMapping = {sort: 's', limit: 'l', fields: 'f', skip: 'sk'}; 37 | var optionsTranslated = {}; 38 | 39 | if (options && !angular.equals(options, {})) { 40 | angular.forEach(optionsMapping, function (targetOption, sourceOption) { 41 | if (angular.isDefined(options[sourceOption])) { 42 | if (angular.isObject(options[sourceOption])) { 43 | optionsTranslated[targetOption] = JSON.stringify(options[sourceOption]); 44 | } else { 45 | optionsTranslated[targetOption] = options[sourceOption]; 46 | } 47 | } 48 | }); 49 | } 50 | return optionsTranslated; 51 | }; 52 | 53 | var requestParams = angular.extend({}, defaultParams, preparyQueryParam(queryJson), prepareOptions(options)); 54 | 55 | return $http.get(collectionUrl, {params: requestParams}).then(resourcesArrayRespTransform); 56 | }; 57 | 58 | Resource.all = function (options, successcb, errorcb) { 59 | return Resource.query({}, options || {}); 60 | }; 61 | 62 | Resource.count = function (queryJson) { 63 | return $http.get(collectionUrl, { 64 | params: angular.extend({}, defaultParams, preparyQueryParam(queryJson), {c: true}) 65 | }).then(function(response){ 66 | return response.data; 67 | }); 68 | }; 69 | 70 | Resource.distinct = function (field, queryJson) { 71 | return $http.post(dbUrl + '/runCommand', angular.extend({}, queryJson || {}, { 72 | distinct: collectionName, 73 | key: field}), { 74 | params: defaultParams 75 | }).then(function (response) { 76 | return response.data.values; 77 | }); 78 | }; 79 | 80 | Resource.getById = function (id) { 81 | return $http.get(collectionUrl + '/' + id, {params: defaultParams}).then(resourceRespTransform); 82 | }; 83 | 84 | Resource.getByObjectIds = function (ids) { 85 | var qin = []; 86 | angular.forEach(ids, function (id) { 87 | qin.push({$oid: id}); 88 | }); 89 | return Resource.query({_id: {$in: qin}}); 90 | }; 91 | 92 | //instance methods 93 | 94 | Resource.prototype.$id = function () { 95 | if (this._id && this._id.$oid) { 96 | return this._id.$oid; 97 | } else if (this._id) { 98 | return this._id; 99 | } 100 | }; 101 | 102 | Resource.prototype.$save = function () { 103 | return $http.post(collectionUrl, this, {params: defaultParams}).then(resourceRespTransform); 104 | }; 105 | 106 | Resource.prototype.$update = function () { 107 | return $http.put(collectionUrl + "/" + this.$id(), angular.extend({}, this, {_id: undefined}), {params: defaultParams}) 108 | .then(resourceRespTransform); 109 | }; 110 | 111 | Resource.prototype.$saveOrUpdate = function () { 112 | return this.$id() ? this.$update() : this.$save(); 113 | }; 114 | 115 | Resource.prototype.$remove = function () { 116 | return $http['delete'](collectionUrl + "/" + this.$id(), {params: defaultParams}).then(resourceRespTransform); 117 | }; 118 | 119 | 120 | return Resource; 121 | } 122 | 123 | return MmongolabResourceFactory; 124 | }]); 125 | -------------------------------------------------------------------------------- /test/angular-mongolab.spec.js: -------------------------------------------------------------------------------- 1 | angular.module('test', ['mongolabResourceHttp']) 2 | .constant('MONGOLAB_CONFIG', {API_KEY: 'testkey', DB_NAME: 'testdb'}) 3 | .factory('Project', function ($mongolabResourceHttp) { 4 | return $mongolabResourceHttp('projects'); 5 | }); 6 | 7 | describe('mongolabResourceHttp', function () { 8 | 9 | var MONGLAB_DB_URL_PREFIX = 'https://api.mongolab.com/api/1/databases/testdb/'; 10 | 11 | var Project; 12 | var testProject = {'_id': {'$oid': 1}, 'key': 'value'}; 13 | var $httpBackend, resultPromise; 14 | 15 | var collectionUrl = function (urlPart, queryPart) { 16 | return MONGLAB_DB_URL_PREFIX + 'collections/projects' + (urlPart || '') + '?apiKey=testkey' + (queryPart || ''); 17 | }; 18 | 19 | var runCommandUrl = function () { 20 | return MONGLAB_DB_URL_PREFIX + 'runCommand?apiKey=testkey'; 21 | }; 22 | 23 | beforeEach(module('test')); 24 | beforeEach(inject(function (_$httpBackend_, _Project_) { 25 | $httpBackend = _$httpBackend_; 26 | Project = _Project_; 27 | })); 28 | beforeEach(function () { 29 | this.addMatchers({ 30 | toHaveSamePropertiesAs: function (expected) { 31 | return angular.equals(expected, this.actual); 32 | } 33 | }); 34 | }); 35 | 36 | describe('class methods', function () { 37 | it("should issue GET request for a query without parameters", function() { 38 | $httpBackend.expect('GET', collectionUrl()).respond([testProject]); 39 | Project.query({}).then(function (queryResult) { 40 | resultPromise = queryResult; 41 | }); 42 | $httpBackend.flush(); 43 | expect(resultPromise.length).toEqual(1); 44 | expect(resultPromise[0]).toHaveSamePropertiesAs(testProject); 45 | }); 46 | 47 | it("should issue GET request with sort options", function() { 48 | $httpBackend.expect('GET', collectionUrl('', '&s=%7B%22priority%22:1%7D')).respond([testProject]); 49 | Project.query({}, {sort: {priority: 1}}).then(function (queryResult) { 50 | resultPromise = queryResult; 51 | }); 52 | $httpBackend.flush(); 53 | expect(resultPromise.length).toEqual(1); 54 | expect(resultPromise[0]).toHaveSamePropertiesAs(testProject); 55 | }); 56 | 57 | it("should issue GET request with limit options", function() { 58 | $httpBackend.expect('GET', collectionUrl('', '&l=10')).respond([testProject]); 59 | Project.query({}, {limit: 10}).then(function (queryResult) { 60 | resultPromise = queryResult; 61 | }); 62 | $httpBackend.flush(); 63 | expect(resultPromise.length).toEqual(1); 64 | expect(resultPromise[0]).toHaveSamePropertiesAs(testProject); 65 | }); 66 | 67 | it("should issue GET request with sort and limit options", function() { 68 | $httpBackend.expect('GET', collectionUrl('', '&l=10&s=%7B%22priority%22:1%7D')).respond([testProject]); 69 | Project.query({}, {sort: {priority: 1}, limit: 10}).then(function (queryResult) { 70 | resultPromise = queryResult; 71 | }); 72 | $httpBackend.flush(); 73 | expect(resultPromise.length).toEqual(1); 74 | expect(resultPromise[0]).toHaveSamePropertiesAs(testProject); 75 | }); 76 | 77 | it("should issue GET all with sort options", function() { 78 | $httpBackend.expect('GET', collectionUrl('', '&s=%7B%22priority%22:1%7D')).respond([testProject]); 79 | Project.all({sort: {priority: 1}}).then(function (queryResult) { 80 | resultPromise = queryResult; 81 | }); 82 | $httpBackend.flush(); 83 | expect(resultPromise.length).toEqual(1); 84 | expect(resultPromise[0]).toHaveSamePropertiesAs(testProject); 85 | }); 86 | 87 | it("should issue GET all", function() { 88 | $httpBackend.expect('GET', collectionUrl()).respond([testProject]); 89 | Project.all().then(function (queryResult) { 90 | resultPromise = queryResult; 91 | }); 92 | $httpBackend.flush(); 93 | expect(resultPromise.length).toEqual(1); 94 | expect(resultPromise[0]).toHaveSamePropertiesAs(testProject); 95 | }); 96 | 97 | it('should issue GET request for distinct calls', function() { 98 | $httpBackend.expect('POST', runCommandUrl()).respond({values: ['value']}); 99 | Project.distinct('name', {}).then(function (queryResult) { 100 | resultPromise = queryResult; 101 | }); 102 | $httpBackend.flush(); 103 | expect(resultPromise).toEqual(['value']); 104 | }); 105 | 106 | it("should issue GET request and return one element for getById", function() { 107 | $httpBackend.expect('GET', collectionUrl('/1')).respond(testProject); 108 | Project.getById('1').then(function (queryResult) { 109 | resultPromise = queryResult; 110 | }); 111 | $httpBackend.flush(); 112 | expect(resultPromise).toHaveSamePropertiesAs(testProject); 113 | }); 114 | 115 | it("should issue GET request and return an array for getByObjectIds", function() { 116 | $httpBackend.expect('GET', collectionUrl('', '&q=%7B%22_id%22:%7B%22$in%22:%5B%7B%22$oid%22:1%7D%5D%7D%7D')).respond([testProject]); 117 | Project.getByObjectIds([1]).then(function (queryResult) { 118 | resultPromise = queryResult; 119 | }); 120 | $httpBackend.flush(); 121 | expect(resultPromise[0]).toHaveSamePropertiesAs(testProject); 122 | expect(resultPromise.length).toEqual(1); 123 | }); 124 | 125 | it('should issue GET request and return a single number for count', function() { 126 | var countResult, countCBResult; 127 | $httpBackend.expect('GET', collectionUrl('', '&c=true&q=%7B%22k%22:%22v%22%7D')).respond(200, 5); 128 | Project.count({k: 'v'}).then(function (result) { 129 | countResult = result; 130 | }); 131 | 132 | $httpBackend.flush(); 133 | expect(countResult).toEqual(5); 134 | }); 135 | }); 136 | 137 | describe('instance methods', function () { 138 | 139 | var flushAndVerify = function () { 140 | $httpBackend.flush(); 141 | expect(resultPromise).toHaveSamePropertiesAs(testProject); 142 | }; 143 | 144 | it('should return undefined $id for new resources', function() { 145 | var project = new Project(); 146 | expect(project.$id()).toBeUndefined(); 147 | }); 148 | 149 | it('should return MongoDB $id if defined', function() { 150 | var project = new Project({_id: {$oid: 'testid'}}); 151 | expect(project.$id()).toEqual('testid'); 152 | }); 153 | 154 | it('should return non standard $id if defined', function() { 155 | var project = new Project({_id: 123456}); 156 | expect(project.$id()).toEqual(123456); 157 | }); 158 | 159 | it('should support saving objects', function() { 160 | $httpBackend.expect('POST', collectionUrl()).respond(testProject); 161 | new Project({key: 'value'}).$save().then(function (data) { 162 | resultPromise = data; 163 | }); 164 | flushAndVerify(); 165 | }); 166 | 167 | it('should save a new object when using $saveOrUpdate', function() { 168 | $httpBackend.expect('POST', collectionUrl()).respond(testProject); 169 | new Project({key: 'value'}).$saveOrUpdate().then(function (data) { 170 | resultPromise = data; 171 | }); 172 | flushAndVerify(); 173 | }); 174 | 175 | it('should update an existing new object when using $saveOrUpdate', function() { 176 | $httpBackend.expect('PUT', collectionUrl('/1')).respond(testProject); 177 | new Project(testProject).$saveOrUpdate().then(function (data) { 178 | resultPromise = data; 179 | }); 180 | flushAndVerify(); 181 | }); 182 | 183 | it('should support updating objects', function() { 184 | $httpBackend.expect('PUT', collectionUrl('/1')).respond(testProject); 185 | new Project(testProject).$update().then(function (data) { 186 | resultPromise = data; 187 | }); 188 | flushAndVerify(); 189 | }); 190 | 191 | it('should support removing objects', function() { 192 | $httpBackend.expect('DELETE', collectionUrl('/1')).respond(testProject); 193 | new Project(testProject).$remove().then(function (data) { 194 | resultPromise = data; 195 | }); 196 | flushAndVerify(); 197 | }); 198 | }); 199 | 200 | describe('non-regression suite', function () { 201 | 202 | it('issue 13 - should properly stringify query string with $or', function () { 203 | $httpBackend.expect('GET', collectionUrl('', 204 | '&q=%7B%22$or%22:%5B%7B%22attrib1%22:%7B%22$regex%22:%22some%22,%22$options%22:%22i%22%7D%7D,%7B%22attrib2%22:%7B%22$regex%22:%22%22,%22$options%22:%22i%22%7D%7D%5D%7D')) 205 | .respond([]); 206 | Project.query({"$or": [ 207 | {"attrib1": {"$regex": "some", "$options": "i"}}, 208 | {"attrib2": {"$regex": "", "$options": "i"}} 209 | ] 210 | }).then(function (queryResult) { 211 | resultPromise = queryResult; 212 | }); 213 | $httpBackend.flush(); 214 | }); 215 | 216 | }); 217 | 218 | afterEach(function () { 219 | $httpBackend.verifyNoOutstandingExpectation(); 220 | $httpBackend.verifyNoOutstandingRequest(); 221 | }); 222 | }); 223 | --------------------------------------------------------------------------------