UNPKG

falcor-router

Version:

A router DataSource constructor for falcor that allows you to model all your cloud data sources as a single JSON resource.

1,025 lines (970 loc) 37.1 kB
var Observable = require('../../../src/RouterRx').Observable; var R = require('../../../src/Router'); var noOp = function() {}; var chai = require('chai'); var expect = chai.expect; var falcor = require('falcor'); var $ref = falcor.Model.ref; var $atom = falcor.Model.atom; var sinon = require("sinon"); var Promise = require("promise"); var doneOnError = require('./../../doneOnError'); var errorOnCompleted = require('./../../errorOnCompleted'); var errorOnNext = require('./../../errorOnNext'); var CallNotFoundError = require('./../../../src/errors/CallNotFoundError'); var CallRequiresPathsError = require('./../../../src/errors/CallRequiresPathsError'); describe('Call', function() { it('should be able to return nothing from a call', function(done) { var router = new R([{ route: 'a.b', call: function(callPath, args) { return undefined; } }]); var onNext = sinon.spy(); router. call(['a', 'b']). do(onNext, noOp, function() { expect(onNext.calledOnce, 'onNext called once').to.be.ok; expect(onNext.getCall(0).args[0]).to.deep.equals({ jsonGraph: {}, paths: [] }); }). subscribe(noOp, done, done); }); it('should be able to return empty array from a call', function(done) { var router = new R([{ route: 'a.b', call: function(callPath, args) { return []; } }]); var onNext = sinon.spy(); router. call(['a', 'b']). do(onNext, noOp, function() { expect(onNext.calledOnce, 'onNext called once').to.be.ok; expect(onNext.getCall(0).args[0]).to.deep.equals({ jsonGraph: {}, paths: [] }); }). subscribe(noOp, done, done); }); it('should call the methodSummary hook when there are errors', function (done) { var i = 0; var router = new R([ { route: "titlesById[{integers:id}].name", get: function(pathSet) { throw new Error('Live or die? Too bad! <HONK>'); } }, { route: 'genrelist[10].titles.push', call: function(callPath, args) { return [ { path: ['genrelist', 10, 'titles', 100], value: { $type: 'ref', value: ['titlesById', 54] } } ]; } }, { route: 'genrelist[10].titles.length', get: function (pathSet) { return [{ path: ['genrelist', 10, 'titles', 'length'], value: 50 }] } } ], { now: function () { return i++; }, hooks: { methodSummary: function (summary) { var expected = { method: 'call', start: 0, end: 10, callPath: ['genrelist', 10, 'titles', 'push'], args: ['title100'], refPaths: [['name']], thisPaths: [['length']], routes: [ { start: 1, route: 'genrelist[10].titles.push', pathSet: ['genrelist', 10, 'titles', 'push'], results: [{ time: 2, value: [ { path: ['genrelist', 10, 'titles', 100], value: { $type: 'ref', value: ['titlesById', 54] } } ] }], end: 3 }, { start: 4, end: 5, route: 'titlesById[{integers:id}].name', pathSet: ['titlesById', 54, 'name'], results: [], error: new Error('Live or die? Too bad! <HONK>') }, { start: 6, end: 8, route: 'genrelist[10].titles.length', pathSet: ['genrelist', 10, 'titles', 'length'], results: [{ time: 7, value: [{ path: ['genrelist', 10, 'titles', 'length'], value: 50 }] }] } ], results: [{ time: 9, value: { jsonGraph: { genrelist: { '10': { titles: { '100': { $type: 'ref', value: ['titlesById', 54] }, length: 50 } } }, titlesById: { '54': { name: { $type: 'error', value: { message: 'Live or die? Too bad! <HONK>' } } } } }, paths: [ ['genrelist', 10, 'titles', 'length'], ['genrelist', 10, 'titles', 100, 'name'] ] } }] }; expect(summary).to.deep.equal(expected); done(); } } }); router.testValue = 1; router.call(['genrelist', 10, 'titles', 'push'], ["title100"], [['name']], [['length']]). subscribe(); }); it('should call the methodSummary hook for path value returns', function (done) { var i = 0; var router = new R([ { route: "titlesById[{integers:id}].name", get: function(pathSet) { return [{ path: ['titlesById', 54, 'name'], value: 'Die Hard' }]; } }, { route: 'genrelist[10].titles.push', call: function(callPath, args) { return [ { path: ['genrelist', 10, 'titles', 100], value: { $type: 'ref', value: ['titlesById', 54] } } ]; } }, { route: 'genrelist[10].titles.length', get: function (pathSet) { return [{ path: ['genrelist', 10, 'titles', 'length'], value: 50 }] } } ], { now: function () { return i++; }, hooks: { methodSummary: function (summary) { var expected = { method: 'call', start: 0, end: 11, callPath: ['genrelist', 10, 'titles', 'push'], args: ['title100'], refPaths: [['name']], thisPaths: [['length']], routes: [ { start: 1, route: 'genrelist[10].titles.push', pathSet: ['genrelist', 10, 'titles', 'push'], results: [{ time: 2, value: [ { path: ['genrelist', 10, 'titles', 100], value: { $type: 'ref', value: ['titlesById', 54] } } ] }], end: 3 }, { start: 4, end: 6, route: 'titlesById[{integers:id}].name', pathSet: ['titlesById', 54, 'name'], results: [{ time: 5, value: [{ path: ['titlesById', 54, 'name'], value: 'Die Hard'}] }] }, { start: 7, end: 9, route: 'genrelist[10].titles.length', pathSet: ['genrelist', 10, 'titles', 'length'], results: [{ time: 8, value: [{ path: ['genrelist', 10, 'titles', 'length'], value: 50 }] }] } ], results: [{ time: 10, value: { jsonGraph: { genrelist: { '10': { titles: { '100': { $type: 'ref', value: ['titlesById', 54] }, length: 50 } } }, titlesById: { '54': { name: 'Die Hard' } } }, paths: [ ['genrelist', 10, 'titles', 'length'], ['genrelist', 10, 'titles', 100, 'name'] ] } }] }; expect(summary).to.deep.equal(expected); done(); } } }); router.testValue = 1; router.call(['genrelist', 10, 'titles', 'push'], ["title100"], [['name']], [['length']]). subscribe(); }); it('should bind "this" properly on a call that tranverses through a reference.', function(done) { var values = []; var router = new R([ { route: "genrelist.myList", get: function(pathSet) { values.push(this.testValue); return [{ path: ['genrelist', 'myList'], value: $ref(['genrelist', 10]) }]; } }, { route: 'genrelist[10].titles.push', call: function(callPath, args) { values.push(this.testValue); return [ { path: ['genrelist', 10, 'titles', 100], value: "title100" }, { path: ['genrelist',10, 'titles', 'length'], value: 101 } ]; } } ]); router.testValue = 1; router.call(['genrelist', 'myList', 'titles', 'push'], ["title100"]). do(noOp, noOp, function() { expect(values).to.deep.equals([1, 1]); }). subscribe(noOp, done, done); }); it('should return invalidations.', function(done) { var router = new R([{ route: 'genrelist[{integers:indices}].titles.remove', call: function(callPath, args) { return callPath.indices.reduce(function(acc, genreIndex) { return acc.concat([ { path: ['genrelist', genreIndex, 'titles', {from: 2, to: 2} ], invalidated: true }, { path: ['genrelist', genreIndex, 'titles', 'length'], value: 2 } ]); }, []); } }]); var onNext = sinon.spy(); router. call(['genrelist', 0, 'titles', 'remove'], [1]). do(onNext, noOp, function() { expect(onNext.calledOnce).to.be.ok; expect(onNext.getCall(0).args[0]).to.deep.equals({ "invalidated": [ [ "genrelist", 0, "titles", { "from": 2, "to": 2 } ] ], "jsonGraph": { "genrelist": { "0": { "titles": { "length": 2 } } } }, "paths": [ [ "genrelist", 0, "titles", "length" ] ] }); }). subscribe(noOp, done, done); }); it('should onError when a Promise.reject of Error is returned from call.', function(done) { var router = new R([{ route: 'videos[{integers:id}].rating', call: function(callPath, args) { return Promise.reject(new Error("Oops?")); } }]); var onError = sinon.spy(); var onNext = sinon.spy(); router. call(['videos', 1234, 'rating'], [5]). do(onNext, onError). do(noOp, function() { expect(onNext.callCount).to.equal(0); expect(onError.getCall(0).args[0].message).to.equal('Oops?'); }). subscribe(noOp, doneOnError(done), errorOnCompleted(done)); }); it('should execute error hooks when an error occurs.', function(done) { var callCount = 0; var callContext = null; var callArgs = null; var router = new R([{ route: 'videos[{integers:id}].rating', call: function(callPath, args) { return Promise.reject(new Error("Oops?")); } }], { hooks: { error: function () { callCount++; callArgs = Array.prototype.slice.call(arguments, 0); callContext = this; } } }); router. call(['videos', 1234, 'rating'], [5]). do(noOp, function(err) { expect(callCount).to.equal(1); expect(callArgs).to.deep.equal([err]); expect(callContext).to.equal(router); }). subscribe(noOp, doneOnError(done), errorOnCompleted(done)); }); it('should onError when an Observable.throw of Error is returned from call.', function(done) { var router = new R([{ route: 'videos[{integers:id}].rating', call: function(callPath, args) { return Observable.throw(new Error("Oops?")); } }]); var onError = sinon.spy(); var onNext = sinon.spy(); router. call(['videos', 1234, 'rating'], [5]). do(onNext, onError). do(noOp, function() { expect(onNext.callCount).to.equal(0); expect(onError.getCall(0).args[0].message).to.equal('Oops?'); }). subscribe(noOp, doneOnError(done), errorOnCompleted(done)); }); it('should return paths in jsonGraphEnvelope if route returns a promise of jsonGraphEnvelope with paths.', function(done) { var onNext = sinon.spy(); var router = new R([{ route: 'genrelist[{integers:indices}].titles.push', call: function(callPath, args) { return Promise.resolve({ "jsonGraph": { "genrelist": { "0": { "titles": { "18": { "$type": "ref", "value": ["titlesById", 1] }, "length": 19 } } } }, "paths": [ ["genrelist", 0, "titles", ["18", "length"]] ] }); } }]); router. call(['genrelist', 0, 'titles', 'push'], [{$type: "ref", value: ['titlesById', 1]}], [], []). do(onNext). do(noOp, noOp, function() { expect(onNext.calledOnce).to.be.ok; expect(onNext.getCall(0).args[0]).to.deep.equals({ "jsonGraph": { "genrelist": { "0": { "titles": { "18": { "$type": "ref", "value": ["titlesById", 1] }, "length": 19 } } } }, "paths": [ ["genrelist", 0, "titles", [18, "length"]] ] }); }). subscribe(noOp, done, done); }); it('should completely onError when an error is thrown from call.', function(done) { getRouter(true, true). call(['videos', 1234, 'rating'], [5]). do(function() { throw new Error('Should not be called. onNext'); }, function(x) { expect(x.message).to.equal('Oops?'); }, function() { throw new Error('Should not be called. onCompleted'); }). subscribe(noOp, function(e) { if (e.message === 'Oops?') { done(); return; } done(e); }); }); it('should cause the router to on error only.', function(done) { getRouter(true). call(['videos', 1234, 'rating'], [5]). do(noOp, function(x) { expect(x instanceof CallRequiresPathsError).to.be.ok; }). subscribe( errorOnNext(done), doneOnError(done), errorOnCompleted(done) ); }); it('should return paths in jsonGraphEnvelope if array of pathValues is returned from promise.', function(done) { var onNext = sinon.spy(); var router = new R([{ route: 'genrelist[{integers:indices}].titles.push', call: function(callPath, args) { return Promise.resolve([ { "path": ["genrelist", 0, "titles", 18], "value": { "$type": "ref", "value": ["titlesById", 1] } }, { "path": ["genrelist", 0, "titles", "length"], "value": 19 } ]); } }]); router. call(['genrelist', 0, 'titles', 'push'], [{$type: "ref", value: ['titlesById', 1]}], [], []). do(onNext). do(noOp, noOp, function() { expect(onNext.calledOnce).to.be.ok; expect(onNext.getCall(0).args[0]).to.deep.equals({ "jsonGraph": { "genrelist": { "0": { "titles": { "18": { "$type": "ref", "value": ["titlesById", 1] }, "length": 19 } } } }, "paths": [ ["genrelist", 0, "titles", [18, "length"]] ] }); }). subscribe(noOp, done, done); }); it('should perform a simple call.', function(done) { var onNext = sinon.spy(); getRouter(). call(['videos', 1234, 'rating'], [5]). do(onNext). do(noOp, noOp, function() { expect(onNext.calledOnce).to.be.ok; expect(onNext.getCall(0).args[0]).to.deep.equals({ jsonGraph: { videos: { 1234: { rating: 5 } } }, paths: [['videos', 1234, 'rating']] }); }). subscribe(noOp, done, done); }); it('should pass the #30 base call test with only suffix.', function(done) { var onNext = sinon.spy(); getExtendedRouter(). call(['lolomo', 'pvAdd'], ['Thrillers'], [['name']]). do(onNext). do(noOp, noOp, function() { expect(onNext.calledOnce).to.be.ok; expect(onNext.getCall(0).args[0]).to.deep.equals({ jsonGraph: { lolomo: $ref('lolomos[123]'), lolomos: { 123: { 0: $ref('listsById[0]') } }, listsById: { 0: { name: 'Thrillers' } } }, paths: [ ['lolomo', 0, 'name'] ] }); }). subscribe(noOp, done, done); }); it('should pass the #30 base call test with only paths.', function(done) { var called = 0; getExtendedRouter(). call(['lolomo', 'pvAdd'], ['Thrillers'], null, [['length']]). do(function(jsongEnv) { expect(jsongEnv).to.deep.equals({ jsonGraph: { lolomo: $ref('lolomos[123]'), lolomos: { 123: { 0: $ref('listsById[0]'), length: 1 } } }, paths: [ ['lolomo', 'length'], ['lolomos', 123, '0'] ] }); ++called; }). subscribe(noOp, done, function() { expect(called).to.equals(1); done(); }); }); it('should pass the #30 base call test with both paths and suffixes.', function(done) { var called = 0; getExtendedRouter(). call(['lolomo', 'pvAdd'], ['Thrillers'], [['name']], [['length']]). do(function(jsongEnv) { expect(jsongEnv).to.deep.equals({ jsonGraph: { lolomo: $ref('lolomos[123]'), lolomos: { 123: { 0: $ref('listsById[0]'), length: 1 } }, listsById: { 0: { name: 'Thrillers' } } }, paths: [ ['lolomo', 'length'], ['lolomo', 0, 'name'] ] }); ++called; }). subscribe(noOp, done, function() { expect(called).to.equals(1); done(); }); }); it('should allow item to be pushed onto collection.', function(done) { var onNext = sinon.spy(); getCallRouter(). call(['genrelist', 0, 'titles', 'push'], [{ $type: 'ref', value: ['titlesById', 1] }]). do(onNext). do(noOp, noOp, function(x) { expect(onNext.called).to.be.ok; expect(onNext.getCall(0).args[0]).to.deep.equals({ jsonGraph: { genrelist: { 0: { titles: { 2: { $type: 'ref', value: ['titlesById', 1] } } } } }, paths: [ ['genrelist', 0, 'titles', '2'] ] }); }, noOp, function() { expect(onNext.calledOnce).to.be.ok; }). subscribe(noOp, done, done); }); it('should evaluate path suffixes on result of a function that adds an item to a collection.', function(done) { var called = 0; var onNext = sinon.spy(); getCallRouter(). call(['genrelist', 0, 'titles', 'push'], [{ $type: 'ref', value: ['titlesById', 1] }], [['name'], ['rating']]). do(onNext). do(noOp, noOp, function(x) { expect(onNext.called).to.be.ok; expect(onNext.getCall(0).args[0]).to.deep.equals({ jsonGraph: { genrelist: { 0: { titles: { 2: { $type: 'ref', value: ['titlesById', 1] } } } }, titlesById: { 1: { name: 'Orange is the new Black', rating: 5 } } }, paths: [ ['genrelist', 0, 'titles', 2, ['name', 'rating']] ] }); ++called; }). subscribe(noOp, done, function() { expect(called).to.equals(1); done(); }); }); it('should throw when calling a function that does not exist.', function(done) { var router = new R([]); var onError = sinon.spy(); router. call(['videos', 1234, 'rating'], [5]). do(noOp, onError). do(noOp, function() { expect(onError.calledOnce).to.be.ok; var args = onError.getCall(0).args; expect(args[0] instanceof CallNotFoundError).to.be.ok; }). subscribe( errorOnNext(done), doneOnError(done), errorOnCompleted(done) ); }); it('should throw when calling a function that does not exist, but get handler does.', function(done) { var router = new R([{ route: 'videos[1234].rating', get: function() { } }]); var onError = sinon.spy(); router. call(['videos', 1234, 'rating'], [5]). do(noOp, onError). do(noOp, function() { expect(onError.calledOnce).to.be.ok; var args = onError.getCall(0).args; expect(args[0] instanceof CallNotFoundError).to.be.ok; }). subscribe( errorOnNext(done), doneOnError(done), errorOnCompleted(done) ); }); function getCallRouter() { return new R([{ route: 'genrelist[{integers}].titles.push', call: function(callPath, args) { return { path: ['genrelist', 0, 'titles', 2], value: { $type: 'ref', value: ['titlesById', 1] } }; } }, { route: 'genrelist[{integers}].titles[{integers}]', get: function(pathSet) { return { path: ['genrelist', 0, 'titles', 1], value: { $type: 'ref', value: ['titlesById', 1] } }; } }, { route: 'titlesById[{integers}]["name", "rating"]', get: function(callPath, args) { return [ { path: ['titlesById', 1, 'name'], value: 'Orange is the new Black' }, { path: ['titlesById', 1, 'rating'], value: 5 } ]; } }]); } function getRouter(noPaths, throwError) { return new R([{ route: 'videos[{integers:id}].rating', call: function(callPath, args) { if (throwError) { throw new Error('Oops?'); } return { jsonGraph: { videos: { 1234: { rating: args[0] } } }, paths: !noPaths && [['videos', 1234, 'rating']] || undefined }; } }]); } function getExtendedRouter(initialIdsAndNames) { var listsById = {}; var idsAndNames = initialIdsAndNames || {}; Object.keys(idsAndNames).reduce(function(acc, id) { var name = idsAndNames[id]; listsById[id] = {name: name, rating: 3}; return acc; }, listsById); function listsLength() { return Object.keys(listsById).length; } function addToList(name) { var length = listsLength(); listsById[length] = { name: name, rating: 5 }; return length; } return new R([{ route: 'lolomo', get: function() { return { path: ['lolomo'], value: $ref('lolomos[123]') }; } }, { route: 'lolomos[{keys:ids}][{integers:indices}]', get: function(alais) { var id = alais.ids[0]; return Observable. from(alais.indices). map(function(idx) { if (listsById[idx]) { return { path: ['lolomos', id, idx], value: $ref(['listsById', idx]) }; } return { path: ['lolomos', id], value: $atom(undefined) }; }); } }, { route: 'lolomos[{keys:ids}].length', get: function(alias) { var id = alias.ids[0]; return { path: ['lolomos', id, 'length'], value: listsLength() }; } }, { route: 'listsById[{integers:indices}].name', get: function(alais) { return Observable. from(alais.indices). map(function(idx) { if (listsById[idx]) { return { path: ['listsById', idx, 'name'], value: listsById[idx].name }; } return { path: ['listsById', idx], value: $atom(undefined) }; }); } }, { route: 'listsById[{integers:indices}].invalidate', call: function(alias, args) { var indices = alias.indices; return indices.map(function(idx) { return { path: ['listsById', idx, 'name'] }; }); } }, { route: 'listsById[{integers:indices}].rating', get: function(alais) { return Observable. from(alais.indices). map(function(idx) { if (listsById[idx]) { return { path: ['listsById', idx, 'rating'], value: listsById[idx].rating }; } return { path: ['listsById', idx], value: $atom(undefined) }; }); } }, { route: 'lolomos[{keys:ids}].pvAdd', call: function(callPath, args) { var id = callPath.ids[0]; var idx = addToList(args[0]); return { path: ['lolomos', id, idx], value: $ref(['listsById', idx]) }; } }, { route: 'lolomos[{keys:ids}].jsongAdd', call: function(callPath, args) { var id = callPath.ids[0]; var idx = addToList(args[0]); var lolomos = {}; lolomos[id] = {}; lolomos[id][idx] = $ref(['listsById', idx]); return { jsonGraph: { lolomos: lolomos }, paths: [['lolomos', id, idx]] }; } }]); } });