'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);
});
});
});'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 } } });
};
});