Commit f2c25e3b authored by Nick Stenning's avatar Nick Stenning

Strip internal properties before sending data to the server

Since angular/angular.js@c054288c9722875e3595e6e6162193e0fb67a251,
`angular.toJson` only strips properties beginning with `$$`, and not
`$`. We set properties on annotations (such as `$orphan`) so need to
reimplement the earlier behaviour.

I've also taken this opportunity to translate the store service to
JavaScript.
parent 701b393c
...@@ -22,7 +22,7 @@ module.exports = class AnnotationViewerController ...@@ -22,7 +22,7 @@ module.exports = class AnnotationViewerController
$scope.search.update = (query) -> $scope.search.update = (query) ->
$location.path('/stream').search('q', query) $location.path('/stream').search('q', query)
store.AnnotationResource.read id: id, (annotation) -> store.AnnotationResource.get id: id, (annotation) ->
annotationMapper.loadAnnotations([annotation]) annotationMapper.loadAnnotations([annotation])
$scope.threadRoot = {children: [threading.idTable[id]]} $scope.threadRoot = {children: [threading.idTable[id]]}
store.SearchResource.get references: id, ({rows}) -> store.SearchResource.get references: id, ({rows}) ->
......
...@@ -151,7 +151,6 @@ module.exports = angular.module('h', [ ...@@ -151,7 +151,6 @@ module.exports = angular.module('h', [
.service('render', require('./render')) .service('render', require('./render'))
.service('searchFilter', require('./search-filter')) .service('searchFilter', require('./search-filter'))
.service('session', require('./session')) .service('session', require('./session'))
.service('store', require('./store'))
.service('streamFilter', require('./stream-filter')) .service('streamFilter', require('./stream-filter'))
.service('tags', require('./tags')) .service('tags', require('./tags'))
.service('time', require('./time')) .service('time', require('./time'))
...@@ -160,6 +159,7 @@ module.exports = angular.module('h', [ ...@@ -160,6 +159,7 @@ module.exports = angular.module('h', [
.service('viewFilter', require('./view-filter')) .service('viewFilter', require('./view-filter'))
.factory('settings', require('./settings')) .factory('settings', require('./settings'))
.factory('store', require('./store'))
.value('AnnotationSync', require('./annotation-sync')) .value('AnnotationSync', require('./annotation-sync'))
.value('AnnotationUISync', require('./annotation-ui-sync')) .value('AnnotationUISync', require('./annotation-ui-sync'))
......
###*
# @ngdoc service
# @name store
#
# @description
# The `store` service handles the backend calls for the restful API. This is
# created dynamically from the API index as the angular $resource() method
# supports the same keys as the index document. This will make a resource
# constructor for each endpoint eg. store.AnnotationResource() and
# store.SearchResource().
###
module.exports = [
'$http', '$resource', 'settings'
($http, $resource, settings) ->
camelize = (string) ->
string.replace /(?:^|_)([a-z])/g, (_, char) -> char.toUpperCase()
store =
$resolved: false
# We call the API root and it gives back the actions it provides.
$promise: $http.get(settings.apiUrl)
.finally -> store.$resolved = true
.then (response) ->
for name, actions of response.data.links
# For each action name we configure an ng-resource.
# For the search resource, one URL is given for all actions.
# For the annotations, each action has its own URL.
prop = "#{camelize(name)}Resource"
store[prop] = $resource(actions.url or settings.apiUrl, {}, actions)
store
]
'use strict';
var angular = require('angular');
function prependTransform(defaults, transform) {
// We can't guarantee that the default transformation is an array
var result = angular.isArray(defaults) ? defaults.slice(0) : [defaults];
result.unshift(transform);
return result;
}
// stripInternalProperties returns a shallow clone of `obj`, lacking all
// properties that begin with a character that marks them as internal
// (currently '$' or '_');
function stripInternalProperties(obj) {
var result = {};
for (var k in obj) {
if (obj.hasOwnProperty(k) && k[0] !== '$') {
result[k] = obj[k];
}
}
return result;
}
/**
* @ngdoc factory
* @name store
* @description The `store` service handles the backend calls for the restful
* API. This is created dynamically from the document returned at
* API index so as to ensure that URL paths/methods do not go out
* of date.
*
* The service currently exposes two resources:
*
* store.SearchResource, for searching, and
* store.AnnotationResource, for CRUD operations on annotations.
*/
// @ngInject
function store($http, $resource, settings) {
var instance = {};
var defaultOptions = {
transformRequest: prependTransform(
$http.defaults.transformRequest,
stripInternalProperties
)
};
// We call the API root and it gives back the actions it provides.
instance.$resolved = false;
instance.$promise = $http.get(settings.apiUrl)
.finally(function () { instance.$resolved = true; })
.then(function (response) {
var links = response.data.links;
instance.SearchResource = $resource(links.search.url, {}, defaultOptions);
instance.AnnotationResource = $resource(links.annotation.read.url, {}, {
create: angular.extend(links.annotation.create, defaultOptions),
update: angular.extend(links.annotation.update, defaultOptions),
delete: angular.extend(links.annotation.delete, defaultOptions),
});
return instance;
});
return instance;
}
module.exports = store;
...@@ -28,7 +28,7 @@ describe "AnnotationViewerController", -> ...@@ -28,7 +28,7 @@ describe "AnnotationViewerController", ->
$scope: $scope or {search: {}} $scope: $scope or {search: {}}
streamer: streamer or {send: ->} streamer: streamer or {send: ->}
store: store or { store: store or {
AnnotationResource: {read: sinon.spy()}, AnnotationResource: {get: sinon.spy()},
SearchResource: {get: ->}} SearchResource: {get: ->}}
streamFilter: streamFilter or { streamFilter: streamFilter or {
setMatchPolicyIncludeAny: -> {addClause: -> {addClause: ->}} setMatchPolicyIncludeAny: -> {addClause: -> {addClause: ->}}
...@@ -43,4 +43,4 @@ describe "AnnotationViewerController", -> ...@@ -43,4 +43,4 @@ describe "AnnotationViewerController", ->
it "calls the annotation API to get the annotation", -> it "calls the annotation API to get the annotation", ->
{store} = createAnnotationViewerController({}) {store} = createAnnotationViewerController({})
assert store.AnnotationResource.read.args[0][0].id == "test_annotation_id" assert store.AnnotationResource.get.args[0][0].id == "test_annotation_id"
{module, inject} = angular.mock
describe 'store', ->
$httpBackend = null
sandbox = null
store = null
before ->
angular.module('h', ['ngResource'])
.service('store', require('../store'))
beforeEach module('h')
beforeEach module ($provide) ->
sandbox = sinon.sandbox.create()
$provide.value 'settings', {apiUrl: 'http://example.com/api'}
return
afterEach ->
sandbox.restore()
beforeEach inject ($q, _$httpBackend_, _store_) ->
$httpBackend = _$httpBackend_
store = _store_
$httpBackend.expectGET('http://example.com/api').respond
links:
annotation:
create: {
method: 'POST'
url: 'http://example.com/api/annotations'
}
delete: {}
read: {}
update: {}
search:
url: 'http://0.0.0.0:5000/api/search'
beware_dragons:
url: 'http://0.0.0.0:5000/api/roar'
$httpBackend.flush()
it 'reads the operations from the backend', ->
assert.isFunction(store.AnnotationResource, 'expected store.AnnotationResource to be a function')
assert.isFunction(store.BewareDragonsResource, 'expected store.BewareDragonsResource to be a function')
assert.isFunction(store.SearchResource, 'expected store.SearchResource to be a function')
it 'saves a new annotation', ->
annotation = { id: 'test'}
annotation = new store.AnnotationResource(annotation)
saved = {}
annotation.$create().then ->
assert.isNotNull(saved.id)
$httpBackend.expectPOST('http://example.com/api/annotations', annotation).respond ->
saved.id = annotation.id
return [201, {}, {}]
$httpBackend.flush()
'use strict';
var inject = angular.mock.inject;
var module = angular.mock.module;
describe('store', function () {
var $httpBackend = null;
var sandbox = null;
var store = null;
before(function () {
angular.module('h', ['ngResource'])
.service('store', require('../store'));
});
beforeEach(module('h'));
beforeEach(module(function ($provide) {
sandbox = sinon.sandbox.create();
$provide.value('settings', {apiUrl: 'http://example.com/api'});
}));
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
sandbox.restore();
});
beforeEach(inject(function ($q, _$httpBackend_, _store_) {
$httpBackend = _$httpBackend_;
store = _store_;
$httpBackend.expectGET('http://example.com/api').respond({
links: {
annotation: {
create: {
method: 'POST',
url: 'http://example.com/api/annotations',
},
delete: {},
read: {},
update: {},
},
search: {
url: 'http://0.0.0.0:5000/api/search',
},
},
});
$httpBackend.flush();
}));
it('reads the operations from the backend', function () {
assert.isFunction(store.AnnotationResource, 'expected store.AnnotationResource to be a function')
assert.isFunction(store.SearchResource, 'expected store.SearchResource to be a function')
});
it('saves a new annotation', function () {
var annotation = new store.AnnotationResource({id: 'test'});
var saved = {};
annotation.$create().then(function () {
assert.isNotNull(saved.id);
});
$httpBackend.expectPOST('http://example.com/api/annotations', {id: 'test'})
.respond(function () {
saved.id = annotation.id;
return [201, {}, {}];
});
$httpBackend.flush();
});
it('removes internal properties before sending data to the server', function () {
var annotation = new store.AnnotationResource({
$highlight: true,
$notme: 'nooooo!',
allowed: 123
});
annotation.$create();
$httpBackend.expectPOST('http://example.com/api/annotations', {
allowed: 123
})
.respond(function () { return {id: 'test'}; });
$httpBackend.flush();
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment