diff --git a/src/javascripts/ng-admin/Crud/CrudModule.js b/src/javascripts/ng-admin/Crud/CrudModule.js index 3a90f9ce..ee970f3f 100644 --- a/src/javascripts/ng-admin/Crud/CrudModule.js +++ b/src/javascripts/ng-admin/Crud/CrudModule.js @@ -25,6 +25,7 @@ define(function (require) { CrudModule.controller('FormController', require('ng-admin/Crud/form/FormController')); CrudModule.controller('DeleteController', require('ng-admin/Crud/delete/DeleteController')); + CrudModule.service('PromisesResolver', require('ng-admin/Crud/misc/PromisesResolver')); CrudModule.service('RetrieveQueries', require('ng-admin/Crud/repository/RetrieveQueries')); CrudModule.service('CreateQueries', require('ng-admin/Crud/repository/CreateQueries')); CrudModule.service('UpdateQueries', require('ng-admin/Crud/repository/UpdateQueries')); diff --git a/src/javascripts/ng-admin/Crud/misc/PromisesResolver.js b/src/javascripts/ng-admin/Crud/misc/PromisesResolver.js new file mode 100644 index 00000000..7aa426d7 --- /dev/null +++ b/src/javascripts/ng-admin/Crud/misc/PromisesResolver.js @@ -0,0 +1,56 @@ +/*global define*/ +define(function () { + 'use strict'; + + function PromisesResolver($q) { + + function allEvenFailed(promises) { + if (!Array.isArray(promises)) { + throw 'allEvenFailed can only handle an array of promises'; + } + var deferred = $q.defer(); + if (promises.length === 0) { + deferred.resolve([]); + return deferred.promise; + } + + var states = []; + var results = []; + + promises.forEach(function (promise, key) { + states[key] = false; // promises are not resolved by default + }); + + promises.forEach(function (promise, key) { + function resolve(result) { + states[key] = true; + results[key] = result; // result may be an error + for (var i in states) { + if (!states[i]) { + return; + } + } + deferred.resolve(results); + } + function resolveSuccess(result) { + return resolve({ status: 'success', result: result }); + } + function resolveError(result) { + return resolve({ status: 'error', error: result }) + } + // whether the promise ends with success or error, consider it done + $q.when(promise).then(resolveSuccess, resolveError); + }); + + return deferred.promise; + }; + + return { + allEvenFailed: allEvenFailed + }; + } + + PromisesResolver.$inject = ['$q']; + + return PromisesResolver; +}); diff --git a/src/javascripts/ng-admin/Crud/repository/CreateQueries.js b/src/javascripts/ng-admin/Crud/repository/CreateQueries.js index 13ff43cc..dcb78a91 100644 --- a/src/javascripts/ng-admin/Crud/repository/CreateQueries.js +++ b/src/javascripts/ng-admin/Crud/repository/CreateQueries.js @@ -32,7 +32,7 @@ define(function (require) { }); }; - CreateQueries.$inject = ['$q', 'Restangular', 'NgAdminConfiguration']; + CreateQueries.$inject = ['$q', 'Restangular', 'NgAdminConfiguration', 'PromisesResolver']; return CreateQueries; }); diff --git a/src/javascripts/ng-admin/Crud/repository/DeleteQueries.js b/src/javascripts/ng-admin/Crud/repository/DeleteQueries.js index 1e1eddb1..ca3f34b3 100644 --- a/src/javascripts/ng-admin/Crud/repository/DeleteQueries.js +++ b/src/javascripts/ng-admin/Crud/repository/DeleteQueries.js @@ -29,7 +29,7 @@ define(function (require) { .customDELETE(); }; - DeleteQueries.$inject = ['$q', 'Restangular', 'NgAdminConfiguration']; + DeleteQueries.$inject = ['$q', 'Restangular', 'NgAdminConfiguration', 'PromisesResolver']; return DeleteQueries; }); diff --git a/src/javascripts/ng-admin/Crud/repository/Queries.js b/src/javascripts/ng-admin/Crud/repository/Queries.js index 359a209c..786b0bbf 100644 --- a/src/javascripts/ng-admin/Crud/repository/Queries.js +++ b/src/javascripts/ng-admin/Crud/repository/Queries.js @@ -10,15 +10,16 @@ define(function () { * @param {Application} Configuration * @constructor */ - function Queries($q, Restangular, Configuration) { + function Queries($q, Restangular, Configuration, PromisesResolver) { this.$q = $q; this.Restangular = Restangular; this.config = Configuration(); + this.PromisesResolver = PromisesResolver; this.Restangular.setFullResponse(true); // To get also the headers } - Queries.$inject = ['$q', 'Restangular', 'NgAdminConfiguration']; + Queries.$inject = ['$q', 'Restangular', 'NgAdminConfiguration', 'PromisesResolver']; return Queries; }); diff --git a/src/javascripts/ng-admin/Crud/repository/RetrieveQueries.js b/src/javascripts/ng-admin/Crud/repository/RetrieveQueries.js index f832cecd..2962ddfb 100644 --- a/src/javascripts/ng-admin/Crud/repository/RetrieveQueries.js +++ b/src/javascripts/ng-admin/Crud/repository/RetrieveQueries.js @@ -160,22 +160,37 @@ define(function (require) { } // Fill all reference entries - return this.$q.all(calls) + return this.PromisesResolver.allEvenFailed(calls) .then(function (responses) { + if (responses.length === 0) { + return references; + } i = 0; + var response; for (j in references) { + reference = references[j]; singleCallFilters = reference.getSingleApiCall(identifiers); // Retrieve entries depending on 1 or many request was done if (singleCallFilters || !rawValues) { - references[j].entries = reference.getReferencedView().mapEntries(responses[i++].data); + response = responses[i++]; + if (response.status == 'error') { + // the response failed + continue; + } + references[j].entries = reference.getReferencedView().mapEntries(response.result.data); } else { entries = []; identifiers = reference.getIdentifierValues(rawValues); for (k in identifiers) { - entries.push(responses[i++]); + response = responses[i++]; + if (response.status == 'error') { + // one of the responses failed + continue; + } + entries.push(response.result); } // Entry are already mapped by getOne @@ -287,7 +302,7 @@ define(function (require) { return entry; }; - RetrieveQueries.$inject = ['$q', 'Restangular', 'NgAdminConfiguration']; + RetrieveQueries.$inject = ['$q', 'Restangular', 'NgAdminConfiguration', 'PromisesResolver']; return RetrieveQueries; }); diff --git a/src/javascripts/ng-admin/Crud/repository/UpdateQueries.js b/src/javascripts/ng-admin/Crud/repository/UpdateQueries.js index 761e24cc..e652b58c 100644 --- a/src/javascripts/ng-admin/Crud/repository/UpdateQueries.js +++ b/src/javascripts/ng-admin/Crud/repository/UpdateQueries.js @@ -36,7 +36,7 @@ define(function (require) { }); }; - UpdateQueries.$inject = ['$q', 'Restangular', 'NgAdminConfiguration']; + UpdateQueries.$inject = ['$q', 'Restangular', 'NgAdminConfiguration', 'PromisesResolver']; return UpdateQueries; }); diff --git a/src/javascripts/test/app-test.js b/src/javascripts/test/app-test.js index 865f1062..ec171c64 100644 --- a/src/javascripts/test/app-test.js +++ b/src/javascripts/test/app-test.js @@ -19,6 +19,7 @@ requirejs.config({ 'angular-mocks': 'bower_components/angular-mocks/angular-mocks', 'mixins': '/base/test/mock/mixins', 'mock/q': '/base/test/mock/q', + 'mock/PromisesResolver': '/base/test/mock/PromisesResolver', 'mock/Restangular': '/base/test/mock/Restangular', 'angular': 'bower_components/angular/angular', diff --git a/src/javascripts/test/mock/PromisesResolver.js b/src/javascripts/test/mock/PromisesResolver.js new file mode 100644 index 00000000..c3c7dced --- /dev/null +++ b/src/javascripts/test/mock/PromisesResolver.js @@ -0,0 +1,9 @@ +/*global jasmine,define*/ + +define('mock/PromisesResolver', ['mixins'], function (mixins) { + "use strict"; + + return { + allEvenFailed: function() { return mixins.buildPromise([]); } + }; +}); diff --git a/src/javascripts/test/unit/Crud/repository/RetrieveQueriesSpec.js b/src/javascripts/test/unit/Crud/repository/RetrieveQueriesSpec.js index e846cb40..e2f444b5 100644 --- a/src/javascripts/test/unit/Crud/repository/RetrieveQueriesSpec.js +++ b/src/javascripts/test/unit/Crud/repository/RetrieveQueriesSpec.js @@ -13,6 +13,7 @@ define(function (require) { ReferenceManyField = require('ng-admin/es6/lib/Field/ReferenceManyField'), Restangular = require('mock/Restangular'), mixins = require('mixins'), + PromisesResolver = require('mock/PromisesResolver'), $q = require('mock/q'), config, cats, @@ -69,195 +70,203 @@ define(function (require) { ]; }); - it('should return all data to display a ListView', function (done) { - spyOn(Restangular, 'getList').and.returnValue(mixins.buildPromise({data: rawCats, headers: function() {}})); - spyOn($q, 'all').and.returnValue(mixins.buildPromise(humans)); + describe('getAll', function() { + it('should return all data to display a ListView', function (done) { + spyOn(Restangular, 'getList').and.returnValue(mixins.buildPromise({data: rawCats, headers: function() {}})); + spyOn(PromisesResolver, 'allEvenFailed').and.returnValue(mixins.buildPromise([{status: 'success', result: humans[0] }, {status: 'success', result: humans[1] }, {status: 'success', result: humans[2] }])); - var retrieveQueries = new RetrieveQueries($q, Restangular, config); + var retrieveQueries = new RetrieveQueries($q, Restangular, config, PromisesResolver); - retrieveQueries.getAll(catView) - .then(function (result) { - expect(result.currentPage).toEqual(1); - expect(result.perPage).toEqual(30); - expect(result.totalItems).toEqual(2); - expect(result.entries.length).toEqual(2); + retrieveQueries.getAll(catView) + .then(function (result) { + expect(result.currentPage).toEqual(1); + expect(result.perPage).toEqual(30); + expect(result.totalItems).toEqual(2); + expect(result.entries.length).toEqual(2); - expect(result.entries[0].values.id).toEqual(1); - expect(result.entries[0].values.name).toEqual('Mizoute'); + expect(result.entries[0].values.id).toEqual(1); + expect(result.entries[0].values.name).toEqual('Mizoute'); - expect(result.entries[0].values.human_id).toEqual(1); - expect(result.entries[0].listValues.human_id).toEqual('Daph'); - }) - .then(done, done.fail); - }); - - it('should return all references values for a View with multiple calls', function (done) { - var retrieveQueries = new RetrieveQueries($q, Restangular, config), - post = new Entity('posts'), - author = new Entity('authors'), - authorRef = new ReferenceField('author'); - - var rawPosts = [ - {id: 1, author: 'abc'}, - {id: 2, author: '19DFE'} - ]; - - var authors = [ - new Entry('author', {id: 'abc', name: 'Rollo'}), - new Entry('author', {id: '19DFE', name: 'Ragna'}) - ]; - - authorRef.targetEntity(author); - authorRef.targetField(new Field('name')); - post.listView() - .addField(authorRef); - - spyOn(Restangular, 'get').and.returnValue(mixins.buildPromise(mixins.buildPromise({}))); - spyOn($q, 'all').and.returnValue(mixins.buildPromise([authors[0], authors[1]])); - - retrieveQueries.getReferencedValues(post.listView().getReferences(), rawPosts) - .then(function (references) { - expect(references.author.entries.length).toEqual(2); - expect(references.author.entries[0].values.id).toEqual('abc'); - expect(references.author.entries[1].values.name).toEqual('Ragna'); - }) - .then(done, done.fail); + expect(result.entries[0].values.human_id).toEqual(1); + expect(result.entries[0].listValues.human_id).toEqual('Daph'); + }) + .then(done, done.fail); + }); }); - it('should return all references values for a View with one call', function (done) { - var retrieveQueries = new RetrieveQueries($q, Restangular, config), - post = new Entity('posts'), - author = new Entity('authors'), - authorRef = new ReferenceField('author'); - - authorRef.singleApiCall(function (ids) { - return { - id: ids - }; + describe('getReferencedValues', function() { + it('should return all references values for a View with multiple calls', function (done) { + var retrieveQueries = new RetrieveQueries($q, Restangular, config, PromisesResolver), + post = new Entity('posts'), + author = new Entity('authors'), + authorRef = new ReferenceField('author'); + + var rawPosts = [ + {id: 1, author: 'abc'}, + {id: 2, author: '19DFE'} + ]; + + var authors = [ + new Entry('author', {id: 'abc', name: 'Rollo'}), + new Entry('author', {id: '19DFE', name: 'Ragna'}) + ]; + + authorRef.targetEntity(author); + authorRef.targetField(new Field('name')); + post.listView() + .addField(authorRef); + + spyOn(Restangular, 'get').and.returnValue(mixins.buildPromise(mixins.buildPromise({}))); + spyOn(PromisesResolver, 'allEvenFailed').and.returnValue(mixins.buildPromise([{status: 'success', result: authors[0] }, { status: 'success', result: authors[1] }])); + + retrieveQueries.getReferencedValues(post.listView().getReferences(), rawPosts) + .then(function (references) { + expect(references.author.entries.length).toEqual(2); + expect(references.author.entries[0].values.id).toEqual('abc'); + expect(references.author.entries[1].values.name).toEqual('Ragna'); + }) + .then(done, done.fail); }); - var rawPosts = [ - {id: 1, author: 'abc'}, - {id: 2, author: '19DFE'} - ]; - - var rawAuthors = [ - {id: 'abc', name: 'Rollo'}, - {id: '19DFE', name: 'Ragna'} - ]; - - var authors = [ - new Entry('authors', rawAuthors[0]), - new Entry('authors', rawAuthors[1]) - ]; + it('should return all references values for a View with one call', function (done) { + var retrieveQueries = new RetrieveQueries($q, Restangular, config, PromisesResolver), + post = new Entity('posts'), + author = new Entity('authors'), + authorRef = new ReferenceField('author'); - authorRef.targetEntity(author); - authorRef.targetField(new Field('name')); - post.listView() - .addField(authorRef); - - spyOn(Restangular, 'getList').and.returnValue(mixins.buildPromise(mixins.buildPromise({}))); - spyOn($q, 'all').and.returnValue(mixins.buildPromise([{data: rawAuthors}])); - - retrieveQueries.getReferencedValues(post.listView().getReferences(), rawPosts) - .then(function (references) { - expect(references.author.entries.length).toEqual(2); - expect(references.author.entries[0].values.id).toEqual('abc'); - expect(references.author.entries[1].values.name).toEqual('Ragna'); - }) - .then(done, done.fail); + authorRef.singleApiCall(function (ids) { + return { + id: ids + }; + }); + + var rawPosts = [ + {id: 1, author: 'abc'}, + {id: 2, author: '19DFE'} + ]; + + var rawAuthors = [ + {id: 'abc', name: 'Rollo'}, + {id: '19DFE', name: 'Ragna'} + ]; + + var authors = [ + new Entry('authors', rawAuthors[0]), + new Entry('authors', rawAuthors[1]) + ]; + + authorRef.targetEntity(author); + authorRef.targetField(new Field('name')); + post.listView() + .addField(authorRef); + + spyOn(Restangular, 'getList').and.returnValue(mixins.buildPromise(mixins.buildPromise({}))); + spyOn(PromisesResolver, 'allEvenFailed').and.returnValue(mixins.buildPromise([{status: 'success', result: { data: rawAuthors }}])); + + retrieveQueries.getReferencedValues(post.listView().getReferences(), rawPosts) + .then(function (references) { + expect(references.author.entries.length).toEqual(2); + expect(references.author.entries[0].values.id).toEqual('abc'); + expect(references.author.entries[1].values.name).toEqual('Ragna'); + }) + .then(done, done.fail); + }); }); - it('should return all referencedLists values for a View', function (done) { - var retrieveQueries = new RetrieveQueries($q, Restangular, config), - state = new Entity('states'), - stateId = new Field('id').identifier(true), - character = new Entity('characters'), - stateCharacters = new ReferencedListField('character'); - - var rawCharacters = [ - {id: 'abc', state_id: 1, name: 'Rollo', age: 35, eyes: 'blue'}, - {id: '19DFE', state_id: 1, name: 'Ragna', age: 33, eyes: 'brown'}, - {id: '1G53a', state_id: 2, name: 'Aelle', age: 45, eyes: 'brown'} - ]; - - stateCharacters - .targetReferenceField('state_id') - .targetFields([ - new Field('id'), - new Field('name'), - new Field('state_id') - ]) - .targetEntity(character); - - state.listView() - .addField(stateId) - .addField(stateCharacters); - - spyOn(Restangular, 'getList').and.returnValue(mixins.buildPromise(mixins.buildPromise({data: rawCharacters}))); - spyOn($q, 'all').and.returnValue(mixins.buildPromise([{data: rawCharacters}])); - - retrieveQueries.getReferencedListValues(state.listView(), null, null, 1) - .then(function (references) { - var entries = references.character.entries; - - expect(entries.length).toEqual(3); - expect(entries[0].values.name).toEqual('Rollo'); - expect(entries[1].values.id).toEqual('19DFE'); - }) - .then(done, done.fail); + describe('getReferencedListValues', function() { + it('should return all referencedLists values for a View', function (done) { + var retrieveQueries = new RetrieveQueries($q, Restangular, config, PromisesResolver), + state = new Entity('states'), + stateId = new Field('id').identifier(true), + character = new Entity('characters'), + stateCharacters = new ReferencedListField('character'); + + var rawCharacters = [ + {id: 'abc', state_id: 1, name: 'Rollo', age: 35, eyes: 'blue'}, + {id: '19DFE', state_id: 1, name: 'Ragna', age: 33, eyes: 'brown'}, + {id: '1G53a', state_id: 2, name: 'Aelle', age: 45, eyes: 'brown'} + ]; + + stateCharacters + .targetReferenceField('state_id') + .targetFields([ + new Field('id'), + new Field('name'), + new Field('state_id') + ]) + .targetEntity(character); + + state.listView() + .addField(stateId) + .addField(stateCharacters); + + spyOn(Restangular, 'getList').and.returnValue(mixins.buildPromise(mixins.buildPromise({data: rawCharacters}))); + spyOn($q, 'all').and.returnValue(mixins.buildPromise([{data: rawCharacters}])); + + retrieveQueries.getReferencedListValues(state.listView(), null, null, 1) + .then(function (references) { + var entries = references.character.entries; + + expect(entries.length).toEqual(3); + expect(entries[0].values.name).toEqual('Rollo'); + expect(entries[1].values.id).toEqual('19DFE'); + }) + .then(done, done.fail); + }); }); - it('should fill reference values of a collection', function () { - var retrieveQueries = new RetrieveQueries({}, Restangular, config), - entry1 = new Entry(), - entry2 = new Entry(), - entry3 = new Entry(), - human = new Entity('humans'), - tag = new Entity('tags'), - ref1 = new ReferenceField('human_id'), - ref2 = new ReferenceManyField('tags'); - - human.editionView().identifier(new Field('id')); - tag.editionView().identifier(new Field('id')); - ref1 - .targetEntity(human) - .targetField(new Field('name')); - ref1.entries = [ - {values: {id: 1, name: 'Bob'}}, - {values: {id: 2, name: 'Daniel'}}, - {values: {id: 3, name: 'Jack'}} - ]; - - ref2 - .targetEntity(tag) - .targetField(new Field('label')); - ref2.entries = [ - {values: {id: 1, label: 'Photo'}}, - {values: {id: 2, label: 'Watch'}}, - {values: {id: 3, label: 'Panda'}} - ]; - - entry1.values.human_id = 1; - entry1.values.tags = [1, 3]; - entry2.values.human_id = 1; - entry2.values.tags = [2]; - entry3.values.human_id = 3; - - var collection = [entry1, entry2, entry3]; - var referencedValues = { - human_id: ref1, - tags: ref2 - }; + describe('fillReferencesValuesFromCollection', function() { + it('should fill reference values of a collection', function () { + var retrieveQueries = new RetrieveQueries({}, Restangular, config, PromisesResolver), + entry1 = new Entry(), + entry2 = new Entry(), + entry3 = new Entry(), + human = new Entity('humans'), + tag = new Entity('tags'), + ref1 = new ReferenceField('human_id'), + ref2 = new ReferenceManyField('tags'); + + human.editionView().identifier(new Field('id')); + tag.editionView().identifier(new Field('id')); + ref1 + .targetEntity(human) + .targetField(new Field('name')); + ref1.entries = [ + {values: {id: 1, name: 'Bob'}}, + {values: {id: 2, name: 'Daniel'}}, + {values: {id: 3, name: 'Jack'}} + ]; + + ref2 + .targetEntity(tag) + .targetField(new Field('label')); + ref2.entries = [ + {values: {id: 1, label: 'Photo'}}, + {values: {id: 2, label: 'Watch'}}, + {values: {id: 3, label: 'Panda'}} + ]; + + entry1.values.human_id = 1; + entry1.values.tags = [1, 3]; + entry2.values.human_id = 1; + entry2.values.tags = [2]; + entry3.values.human_id = 3; + + var collection = [entry1, entry2, entry3]; + var referencedValues = { + human_id: ref1, + tags: ref2 + }; - collection = retrieveQueries.fillReferencesValuesFromCollection(collection, referencedValues, true); + collection = retrieveQueries.fillReferencesValuesFromCollection(collection, referencedValues, true); - expect(collection.length).toEqual(3); - expect(collection[0].listValues.human_id).toEqual('Bob'); - expect(collection[0].listValues.tags).toEqual(['Photo', 'Panda']); - expect(collection[1].listValues.tags).toEqual(['Watch']); - expect(collection[2].listValues.human_id).toEqual('Jack'); - expect(collection[2].listValues.tags).toEqual([]); + expect(collection.length).toEqual(3); + expect(collection[0].listValues.human_id).toEqual('Bob'); + expect(collection[0].listValues.tags).toEqual(['Photo', 'Panda']); + expect(collection[1].listValues.tags).toEqual(['Watch']); + expect(collection[2].listValues.human_id).toEqual('Jack'); + expect(collection[2].listValues.tags).toEqual([]); + }); }); describe("getOne", function () { @@ -291,7 +300,7 @@ define(function (require) { } })); - var retrieveQueries = new RetrieveQueries({}, Restangular, config); + var retrieveQueries = new RetrieveQueries({}, Restangular, config, PromisesResolver); retrieveQueries.getOne(view, 1) .then(function (entry) {