Created
April 26, 2014 11:14
-
-
Save ondrasak/ae024e1b50de02d8100f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 'use strict'; | |
| // | |
| // Implementation of HAL http://stateless.co/hal_specification.html | |
| // | |
| // NB: self link always must exist! | |
| // | |
| angular.module('realty.common.resources', []) | |
| .provider('backend', function () { | |
| var options = { | |
| apiEndpoint: 'http://localhost', | |
| errorHandler: undefined | |
| }; | |
| this.configure = function (opts) { | |
| angular.extend(options, opts); | |
| }; | |
| this.$get = function ($q, $http, $injector) { | |
| var Resource, emptyResource; | |
| Resource = (function () { | |
| var ctr = function (_status_) { | |
| var status = _status_, isLoaded = false; | |
| this._links = {}; | |
| this._embedded = {}; | |
| this._isLoadedFlag = function (enabled) { | |
| if (!isLoaded) { | |
| isLoaded = enabled || false; | |
| return false; | |
| } | |
| return true; | |
| }; | |
| this.getStatus = function () { | |
| return status; | |
| }; | |
| }; | |
| ctr.prototype.constructor = ctr; | |
| ctr.prototype.selfLink = function () { | |
| return this._links.self.href; | |
| }; | |
| // Return true iff resource is dummy | |
| ctr.prototype.isInvalid = function () { | |
| var nLinks, nEmbedded; | |
| nLinks = Object.getOwnPropertyNames(this._links).length; | |
| nEmbedded = Object.getOwnPropertyNames(this._embedded).length; | |
| return (nLinks + nEmbedded) === 0; | |
| }; | |
| ctr.prototype.create = function (relation, data, flags) { | |
| return this._loadSelf(flags).then(function (self) { | |
| return self._sendRequest({ method: 'POST', relation: relation, data: data, flags: flags }); | |
| }); | |
| }; | |
| ctr.prototype.link = function (rel) { | |
| if (this._hasLink(rel)) { | |
| return this._links[rel].href; | |
| } | |
| if (this._hasEmbedded(rel)) { | |
| var data = this._embedded[rel]; | |
| if (angular.isArray(data)) { | |
| data = data[0]; | |
| } | |
| return data._links.self.href; | |
| } | |
| }; | |
| ctr.prototype.readAll = function (query, flags) { | |
| var rel, relations; | |
| relations = query.split(/\s+/); | |
| rel = [].pop.apply(relations); | |
| return this.read.apply(this, relations, flags).then(function (resource) { | |
| if (!rel || !resource._hasEmbedded(rel)) { | |
| return $q.when([]); | |
| } | |
| var i, list = resource._embedded[rel]; | |
| if (!angular.isArray(list)) { | |
| list = [ list ]; | |
| } | |
| for (i = 0; i < list.length; i++) { | |
| list[i] = resource._embeddedToResource(list[i]); | |
| } | |
| return $q.when(list); | |
| }); | |
| }; | |
| ctr.prototype.read = function (query, params, flags) { | |
| var promise, i, args; | |
| args = !query ? [] : query.split(/\s+/); | |
| function makePromise(idx, promise) { | |
| return promise.then(function (resource) { | |
| return resource._readOne(args[idx], params, flags); | |
| }); | |
| } | |
| promise = this._loadSelf(flags); | |
| for (i = 0; i < args.length; i++) { | |
| promise = makePromise(i, promise); | |
| } | |
| return promise; | |
| }; | |
| ctr.prototype.update = function (relation, data, flags) { | |
| return this._loadSelf(flags).then(function (self) { | |
| return self._sendRequest({ method: 'PUT', relation: relation, data: data, flags: flags }); | |
| }); | |
| }; | |
| ctr.prototype.destroy = function (relation, flags) { | |
| return this._loadSelf(flags).then(function (self) { | |
| return self._sendRequest({ method: 'DELETE', relation: relation, flags: flags }); | |
| }); | |
| }; | |
| ctr.prototype._loadSelf = function (_flags_) { | |
| if (this._isLoadedFlag()) { | |
| return $q.when(this); | |
| } | |
| var flags = { skipErrors: true }; | |
| angular.extend(flags, _flags_); | |
| return this._readOne('self', undefined, flags); | |
| }; | |
| ctr.prototype._readOne = function (rel, params, flags) { | |
| if (this._hasEmbedded(rel)) { | |
| var data = this._embedded[rel]; | |
| if (angular.isArray(data)) { | |
| data = data[0]; | |
| } | |
| return $q.when(this._embeddedToResource(data, rel)); | |
| } | |
| return this._sendRequest({ method: 'GET', relation: rel, uriParams: params, flags: flags }); | |
| }; | |
| ctr.prototype._embeddedToResource = function (_data_, rel) { | |
| var resource, data = angular.copy(_data_); | |
| if (!data._links || !data._links.self) { | |
| if (rel && this._hasLink(rel)) { | |
| this._createSelfLinkIfNeeded(data, this._links[rel].href); | |
| } else { | |
| throw new Error('Not found self link for embedded relation ' + rel); | |
| } | |
| } | |
| resource = makeResource(data); | |
| resource._isLoadedFlag(true); | |
| return resource; | |
| }; | |
| ctr.prototype._sendRequest = function (opts) { | |
| var url, self = this, flags = opts.flags || {}; | |
| if (!this._hasLink(opts.relation)) { | |
| return $q.when(emptyResource); | |
| } | |
| url = this._extractUrl(this._links[opts.relation], opts.uriParams); | |
| return $http({ | |
| method: opts.method, | |
| url: url, | |
| data: opts.data, | |
| headers: { | |
| 'Accept': 'application/hal+json' | |
| } | |
| }).then(function (response) { | |
| var data; | |
| data = response.data; | |
| self._createSelfLinkIfNeeded(data, url); | |
| return makeResource(data, response.status); | |
| }, function (response) { | |
| var resource, handler, data; | |
| data = response.data; | |
| self._createSelfLinkIfNeeded(data, url); | |
| resource = makeResource(data, response.status); | |
| if (options.errorHandler && !flags.disableResponseHandler) { | |
| handler = $injector.get(options.errorHandler); | |
| return handler(resource); | |
| } | |
| if (flags.skipErrors) { | |
| return resource; | |
| } | |
| return $q.reject(resource); | |
| }); | |
| }; | |
| ctr.prototype._createSelfLinkIfNeeded = function (data, url) { | |
| if (!data._links) { | |
| data._links = {}; | |
| } | |
| if (!data._links.self) { | |
| angular.extend(data._links, { self: { href: url } }); | |
| } | |
| }; | |
| ctr.prototype._hasEmbedded = function (relation) { | |
| return this._embedded.hasOwnProperty(relation); | |
| }; | |
| ctr.prototype._hasLink = function (relation) { | |
| return this._links.hasOwnProperty(relation); | |
| }; | |
| ctr.prototype._extractUrl = function (link, params) { | |
| /* global UriTemplate */ | |
| var url = link.href, | |
| tpl = new UriTemplate(url); | |
| return link.templated ? tpl.fillFromObject(params) : url; | |
| }; | |
| return ctr; | |
| }()); | |
| emptyResource = new Resource(); | |
| Object.freeze(emptyResource); | |
| function makeResource(_data_, status) { | |
| var data = angular.copy(_data_) || {}, | |
| resource = new Resource(status || 200); | |
| angular.extend(resource, data); | |
| if (angular.isUndefined(resource._embedded)) { | |
| resource._embedded = {}; | |
| } | |
| Object.freeze(resource); | |
| return resource; | |
| } | |
| return makeResource({ _links: { self: { href: options.apiEndpoint } } }); | |
| }; | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 'use strict'; | |
| describe('Rest API resources', function () { | |
| var backend, $httpBackend, apiEndpoint = 'http://localhost'; | |
| beforeEach(function () { | |
| module('realty.common.resources', function (backendProvider) { | |
| backendProvider.configure({ apiEndpoint: apiEndpoint }); | |
| }); | |
| inject(function (_$httpBackend_, _backend_) { | |
| $httpBackend = _$httpBackend_; | |
| backend = _backend_; | |
| }); | |
| }); | |
| afterEach(function () { | |
| $httpBackend.flush(); | |
| $httpBackend.verifyNoOutstandingExpectation(); | |
| $httpBackend.verifyNoOutstandingRequest(); | |
| }); | |
| it('should return root resource', function () { | |
| $httpBackend.expectGET(apiEndpoint).respond({}); | |
| backend.read().then(function (root) { | |
| expect(root.isInvalid()).toBeFalsy(); | |
| expect(root._links.self).toBeDefined(); | |
| expect(root.selfLink() === apiEndpoint).toBeTruthy(); | |
| }); | |
| }); | |
| it('should throws no exception while traversing', function () { | |
| $httpBackend.expectGET(apiEndpoint).respond({}); | |
| backend.read('invalid1 invalid2 invalid3').then(function (resource) { | |
| expect(resource.isInvalid()).toBeTruthy(); | |
| }); | |
| }); | |
| it('should traverse with promises', function () { | |
| $httpBackend.whenGET(apiEndpoint).respond({ _links: { outer: { href: apiEndpoint + '/outer'}}}); | |
| $httpBackend.whenGET(apiEndpoint + '/outer').respond({ _links: { inner: { href: apiEndpoint + '/inner' }}}); | |
| $httpBackend.expectGET(apiEndpoint + '/inner').respond({}); | |
| backend.read('outer').then(function (outer) { | |
| expect(outer.isInvalid()).toBeFalsy(); | |
| expect(outer.selfLink() === (apiEndpoint + '/outer')).toBeTruthy(); | |
| outer.read('inner').then(function (inner) { | |
| expect(inner.isInvalid()).toBeFalsy(); | |
| expect(inner.selfLink() === (apiEndpoint + '/inner')).toBeTruthy(); | |
| }); | |
| }); | |
| }); | |
| it('should traverse resources', function () { | |
| $httpBackend.expectGET(apiEndpoint).respond({ _links: { outer: { href: apiEndpoint + '/outer'}}}); | |
| $httpBackend.expectGET(apiEndpoint + '/outer').respond({ _links: { inner: { href: apiEndpoint + '/inner' }}}); | |
| $httpBackend.expectGET(apiEndpoint + '/inner').respond({}); | |
| backend.read('outer inner').then(function (inner) { | |
| expect(inner.isInvalid()).toBeFalsy(); | |
| expect(inner.selfLink() === (apiEndpoint + '/inner')).toBeTruthy(); | |
| }); | |
| }); | |
| it('should process _embedded property', function () { | |
| $httpBackend.expectGET(apiEndpoint).respond({ _links: { outer: { href: apiEndpoint + '/outer' }}, _embedded: { outer: {} } }); | |
| backend.read('outer').then(function (outer) { | |
| expect(outer.isInvalid()).toBeFalsy(); | |
| expect(outer.selfLink() === (apiEndpoint + '/outer')).toBeTruthy(); | |
| }); | |
| }); | |
| it('should parse templated url', function () { | |
| $httpBackend.expectGET(apiEndpoint).respond({ _links: { find: { href: apiEndpoint + '/items{?colour}', templated: true } } }); | |
| $httpBackend.expectGET(apiEndpoint + '/items?colour=blue').respond({ colour: 'blue' }); | |
| backend.read('find', { colour: 'blue' }).then(function (item) { | |
| expect(item.isInvalid()).toBeFalsy(); | |
| expect(item.colour === 'blue').toBeTruthy(); | |
| }); | |
| }); | |
| describe('when collection is returned', function () { | |
| beforeEach(function () { | |
| $httpBackend.expectGET(apiEndpoint).respond({ | |
| _links: { items: { href: apiEndpoint + '/items' } }, | |
| _embedded: { items: [ | |
| { _links: { self: { href: apiEndpoint + '/items/0' } } }, | |
| { _links: { self: { href: apiEndpoint + '/items/1' } } }, | |
| { _links: { self: { href: apiEndpoint + '/items/2' } } } | |
| ]} | |
| }); | |
| }); | |
| it('should get collection values', function () { | |
| backend.readAll('items').then(function (items) { | |
| var i; | |
| expect(angular.isArray(items)).toBeTruthy(); | |
| expect(items.length).toBe(3); | |
| for (i = 0; i < 3; i++) { | |
| expect(items[i].selfLink() === (apiEndpoint + '/items/' + i)).toBeTruthy(); | |
| } | |
| }); | |
| }); | |
| it('should read first value from collection', function () { | |
| backend.read('items').then(function (item) { | |
| expect(item.isInvalid()).toBeFalsy(); | |
| expect(item.selfLink() === (apiEndpoint + '/items/0')); | |
| }); | |
| }); | |
| }); | |
| it('should create entity', function () { | |
| $httpBackend.expectGET(apiEndpoint).respond({ _links: { items: { href: apiEndpoint + '/items' } } }); | |
| $httpBackend.expectPOST(apiEndpoint + '/items').respond(201, {}); | |
| backend.create('items', { id: 1 }).then(function (items) { | |
| expect(items.isInvalid()).toBeFalsy(); | |
| expect(items.getStatus()).toBe(201); | |
| $httpBackend.expectGET(apiEndpoint).respond({ _links: { items: { href: apiEndpoint + '/items' } } }); | |
| $httpBackend.expectPOST(apiEndpoint + '/items').respond(201, {}); | |
| backend.create('items', { id: 2 }).then(function (items) { | |
| expect(items.isInvalid()).toBeFalsy(); | |
| expect(items.getStatus()).toBe(201); | |
| }); | |
| }); | |
| }); | |
| it('should update entity', function () { | |
| $httpBackend.expectGET(apiEndpoint).respond({ _links: { items: { href: apiEndpoint + '/items' } } }); | |
| $httpBackend.expectPUT(apiEndpoint + '/items').respond({}); | |
| backend.update('items', { id: 1 }).then(function (items) { | |
| expect(items.isInvalid()).toBeFalsy(); | |
| expect(items.getStatus()).toBe(200); | |
| }); | |
| }); | |
| it('should delete entity', function () { | |
| $httpBackend.expectGET(apiEndpoint).respond({ _links: { item: { href: apiEndpoint + '/item' } } }); | |
| $httpBackend.expectDELETE(apiEndpoint + '/item').respond({}); | |
| backend.destroy('item').then(function (item) { | |
| expect(item.isInvalid()).toBeFalsy(); | |
| expect(item.getStatus()).toBe(200); | |
| }); | |
| }); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment