diff --git a/package.json b/package.json index c74713c..729563d 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,6 @@ }, "dependencies": { "array.prototype.find": "^1.0.0", - "rx-lite": "^3.1.2" + "rx": "^4.0.0" } } diff --git a/scripts/build b/scripts/build index b1540ab..46e7658 100755 --- a/scripts/build +++ b/scripts/build @@ -2,6 +2,7 @@ # Node ./node_modules/.bin/babel src/store.js -m common --module-id Store -o dist/store.js +./node_modules/.bin/babel src/ajax.js -m common --module-id ajax -o dist/ajax.js ./node_modules/.bin/babel src/ajax-adapter.js -m common --module-id AjaxAdapter -o dist/ajax-adapter.js # Browser (Development) diff --git a/spec/adapters/ajax/create-spec.js b/spec/adapters/ajax/create-spec.js index b603801..20e4796 100644 --- a/spec/adapters/ajax/create-spec.js +++ b/spec/adapters/ajax/create-spec.js @@ -32,7 +32,7 @@ test("create must post a resource to the server and add it to the store on succe }; request.respond(201, { "Content-Type": "application/vnd.api+json" }, JSON.stringify(data)); }); - store.create("products", { title: "My Book" }, function (product) { + store.create("products", { title: "My Book" }).subscribe(function (product) { t.equal(product.title, "My Book"); t.equal(store.find("products", "9").title, "My Book"); }); @@ -53,7 +53,7 @@ test("create must handle 500 errors for failed attempts", function (t) { "" ]); t.equal(store.find("products").length, 0); - store.create("products", {}, function () { + store.create("products", {}).subscribe(function () { t.fail("must not call the success callback"); }, function () { t.equal(store.find("products").length, 0); @@ -88,85 +88,12 @@ test("create must call the error callback if an error is raised during the proce ] }) ]); - store.create("products", {}, null, callback); + store.create("products", {}).subscribe(function () {}, callback); server.respond(); t.equal(callback.callCount, 1); server.restore(); }); -test("create must handle missing success or error callbacks", function (t) { - var server = sinon.fakeServer.create({ autoRespond: false }); - var adapter = new Store.AjaxAdapter(); - var store = new Store(adapter); - var callback = sinon.spy(); - var context = {}; - t.plan(6); - t.timeoutAfter(1000); - store.define("products", {}); - store.define("categories", {}); - server.respondWith("POST", "/products", [ - 201, - { "Content-Type": "application/vnd.api+json" }, - JSON.stringify({ - data: { type: "products", id: "9" } - }) - ]); - server.respondWith("POST", "/categories", [ - 500, - { "Content-Type": "application/vnd.api+json" }, - "" - ]); - store.create("products", {}, null, callback, context); - server.respond(); - t.equal(store.find("products").length, 1); - t.equal(callback.callCount, 0); - callback.reset(); - store.create("products", {}, callback, context); - server.respond(); - t.equal(callback.callCount, 1); - t.equal(callback.firstCall.thisValue, context); - callback.reset(); - store.create("categories", {}, null, callback, context); - server.respond(); - t.equal(callback.callCount, 1); - t.equal(callback.firstCall.thisValue, context); - server.restore(); -}); - -test("create must call callbacks with the context provided", function (t) { - var server = sinon.fakeServer.create({ autoRespond: false }); - var adapter = new Store.AjaxAdapter(); - var store = new Store(adapter); - var callback = sinon.spy(); - var context = {}; - t.plan(4); - t.timeoutAfter(1000); - store.define("products", {}); - store.define("categories", {}); - server.respondWith("POST", "/products", [ - 201, - { "Content-Type": "application/vnd.api+json" }, - JSON.stringify({ - data: { type: "products", id: "9" } - }) - ]); - server.respondWith("POST", "/categories", [ - 500, - { "Content-Type": "application/vnd.api+json" }, - "" - ]); - store.create("products", {}, callback, function () {}, context); - server.respond(); - t.equal(callback.callCount, 1); - t.equal(callback.firstCall.thisValue, context); - callback.reset(); - store.create("categories", {}, function () {}, callback, context); - server.respond(); - t.equal(callback.callCount, 1); - t.equal(callback.firstCall.thisValue, context); - server.restore(); -}); - test("create must use the adapter's 'base' config if present", function (t) { var server = sinon.fakeServer.create({ autoRespond: false }); var adapter = new Store.AjaxAdapter({ base: "http://example.com" }); @@ -181,7 +108,7 @@ test("create must use the adapter's 'base' config if present", function (t) { data: { type: "products", id: "9" } }) ]); - store.create("products", {}, function (product) { + store.create("products", {}).subscribe(function (product) { t.equal(store.find("products", "9"), product); }); server.respond(); diff --git a/spec/adapters/ajax/destroy-spec.js b/spec/adapters/ajax/destroy-spec.js index a1f80ab..c79b215 100644 --- a/spec/adapters/ajax/destroy-spec.js +++ b/spec/adapters/ajax/destroy-spec.js @@ -18,7 +18,7 @@ test("destroy must delete a resource from the server and remove it from the stor id: "6" }); t.equal(store.find("products").length, 1); - store.destroy("products", "6", function () { + store.destroy("products", "6").subscribe(function () { t.equal(store.find("products").length, 0); }); server.respond(); @@ -44,7 +44,7 @@ test("destroy must handle 500 errors for failed attempts", function (t) { id: "6" }); t.equal(store.find("products").length, 1); - store.destroy("products", "6", function () { + store.destroy("products", "6").subscribe(function () { t.fail("must not call the success callback"); }, function () { t.equal(store.find("products").length, 1); @@ -67,7 +67,7 @@ test("destroy must use the adapter's 'base' config if present", function (t) { ]); store.add({ type: "products", id: "2" }); t.equal(store.find("products").length, 1); - store.destroy("products", "2", function () { + store.destroy("products", "2").subscribe(function () { t.equal(store.find("products").length, 0); }); server.respond(); @@ -87,7 +87,7 @@ test("destroy must call the error callback if an error is raised during the proc { "Content-Type": "application/vnd.api+json" }, "" ]); - store.destroy("products", "1", function () { + store.destroy("products", "1").subscribe(function () { throw new Error(); }, callback); server.respond(); @@ -95,72 +95,6 @@ test("destroy must call the error callback if an error is raised during the proc server.restore(); }); -test("destroy must handle missing success or error callbacks", function (t) { - var server = sinon.fakeServer.create({ autoRespond: false }); - var adapter = new Store.AjaxAdapter(); - var store = new Store(adapter); - var callback = sinon.spy(); - var context = {}; - t.plan(5); - t.timeoutAfter(1000); - store.define("products", {}); - server.respondWith("DELETE", "/products/6", [ - 204, - { "Content-Type": "application/vnd.api+json" }, - "" - ]); - server.respondWith("DELETE", "/products/1", [ - 500, - { "Content-Type": "application/vnd.api+json" }, - "" - ]); - store.destroy("products", "6", null, callback, context); - server.respond(); - t.equal(callback.callCount, 0); - callback.reset(); - store.destroy("products", "6", callback, context); - server.respond(); - t.equal(callback.callCount, 1); - t.equal(callback.firstCall.thisValue, context); - callback.reset(); - store.destroy("products", "1", null, callback, context); - server.respond(); - t.equal(callback.callCount, 1); - t.equal(callback.firstCall.thisValue, context); - server.restore(); -}); - -test("destroy must call callbacks with the context provided", function (t) { - var server = sinon.fakeServer.create({ autoRespond: false }); - var adapter = new Store.AjaxAdapter(); - var store = new Store(adapter); - var callback = sinon.spy(); - var context = {}; - t.plan(4); - t.timeoutAfter(1000); - store.define("products", {}); - server.respondWith("DELETE", "/products/6", [ - 204, - { "Content-Type": "application/vnd.api+json" }, - "" - ]); - server.respondWith("DELETE", "/products/1", [ - 500, - { "Content-Type": "application/vnd.api+json" }, - "" - ]); - store.destroy("products", "6", callback, context); - server.respond(); - t.equal(callback.callCount, 1); - t.equal(callback.firstCall.thisValue, context); - callback.reset(); - store.destroy("products", "1", null, callback, context); - server.respond(); - t.equal(callback.callCount, 1); - t.equal(callback.firstCall.thisValue, context); - server.restore(); -}); - test("destroy must throw an error if the type has not been defined", function (t) { var adapter = new Store.AjaxAdapter(); var store = new Store(adapter); diff --git a/spec/adapters/ajax/load-spec.js b/spec/adapters/ajax/load-spec.js index 9c82b04..099c8cf 100644 --- a/spec/adapters/ajax/load-spec.js +++ b/spec/adapters/ajax/load-spec.js @@ -48,7 +48,7 @@ test("load must fetch a single resource from the server and add it to the store" }; request.respond(200, { "Content-Type": "application/vnd.api+json" }, JSON.stringify(data)); }); - store.load("products", "12", function (product) { + store.load("products", "12").subscribe(function (product) { t.equal(store.find("products", "12"), product); t.equal(store.find("products", "12").title, "An Awesome Book"); t.equal(store.find("products", "12").category, store.find("categories", "6")); @@ -99,7 +99,7 @@ test("load must fetch a collection of resources from the server and add them to ] }) ]); - store.load("products", function (products) { + store.load("products").subscribe(function (products) { t.equal(products.length, 3); t.equal(store.find("products").length, 3); t.deepEqual(store.find("products").map(a => a.title).sort(), [ "A Book", "B Book", "C Book" ]); @@ -123,7 +123,7 @@ test("load must handle 500 errors for failed attempts", function (t) { "" ]); t.equal(store.find("products").length, 0); - store.load("products", "12", function () { + store.load("products", "12").subscribe(function () { t.fail("must not call the success callback"); }, function () { t.equal(store.find("products").length, 0); @@ -158,9 +158,9 @@ test("load must use the adapter's 'base' config if present", function (t) { }) ]); t.equal(store.find("products").length, 0); - store.load("products", "9", function () { + store.load("products", "9").subscribe(function () { t.equal(store.find("products").length, 1); - store.load("products", function () { + store.load("products").subscribe(function () { t.deepEqual(store.find("products").map(x => x.id).sort(), [ "2", "4", "7", "9" ]); }); server.respond(); @@ -187,82 +187,12 @@ test("load must call the error callback if an error is raised during the process ] }) ]); - store.load("products", "1", function () {}, callback); + store.load("products", "1").subscribe(function () {}, callback); server.respond(); t.equal(callback.callCount, 1); server.restore(); }); -test("load must handle missing options, success callbacks or error callbacks", function (t) { - var server = sinon.fakeServer.create({ autoRespond: false }); - var adapter = new Store.AjaxAdapter(); - var store = new Store(adapter); - var callback = sinon.spy(); - var context = {}; - t.plan(5); - t.timeoutAfter(1000); - store.define("products", {}); - server.respondWith("GET", "/products/6", [ - 200, - { "Content-Type": "application/vnd.api+json" }, - JSON.stringify({ - data: { type: "products", id: "6" } - }) - ]); - server.respondWith("GET", "/products/1", [ - 500, - { "Content-Type": "application/vnd.api+json" }, - "" - ]); - store.load("products", "6", {}, null, callback, context); - server.respond(); - t.equal(callback.callCount, 0); - callback.reset(); - store.load("products", "6", callback, context); - server.respond(); - t.equal(callback.callCount, 1); - t.equal(callback.firstCall.thisValue, context); - callback.reset(); - store.load("products", "1", {}, null, callback, context); - server.respond(); - t.equal(callback.callCount, 1); - t.equal(callback.firstCall.thisValue, context); - server.restore(); -}); - -test("load must call callbacks with the context provided", function (t) { - var server = sinon.fakeServer.create({ autoRespond: false }); - var adapter = new Store.AjaxAdapter(); - var store = new Store(adapter); - var callback = sinon.spy(); - var context = {}; - t.plan(4); - t.timeoutAfter(1000); - store.define("products", {}); - server.respondWith("GET", "/products/6", [ - 200, - { "Content-Type": "application/vnd.api+json" }, - JSON.stringify({ - data: { type: "products", id: "6" } - }) - ]); - server.respondWith("GET", "/products/1", [ - 500, - { "Content-Type": "application/vnd.api+json" }, - "" - ]); - store.load("products", "6", {}, callback, context); - server.respond(); - t.equal(callback.callCount, 1); - t.equal(callback.firstCall.thisValue, context); - callback.reset(); - store.load("products", "1", {}, null, callback, context); - server.respond(); - t.equal(callback.callCount, 1); - t.equal(callback.firstCall.thisValue, context); - server.restore(); -}); - test("load must use the options if they're provided", function (t) { var server = sinon.fakeServer.create({ autoRespond: false }); var adapter = new Store.AjaxAdapter(); @@ -286,7 +216,7 @@ test("load must use the options if they're provided", function (t) { sort: "age,name,-created", page: 1, filter: "foo" - }, function (products) { + }).subscribe(function (products) { t.pass("returns a successful response"); }, function (error) { t.fail(error); diff --git a/spec/adapters/ajax/update-spec.js b/spec/adapters/ajax/update-spec.js index 553b5b4..4dd9c4a 100644 --- a/spec/adapters/ajax/update-spec.js +++ b/spec/adapters/ajax/update-spec.js @@ -31,7 +31,7 @@ test("update must update a resource on the server and add reflect the changes in title: "My Book" } }); - store.update("products", "9", { title: "My Book!" }, function (product) { + store.update("products", "9", { title: "My Book!" }).subscribe(function (product) { t.equal(product.title, "My Book!"); t.equal(store.find("products", "9").title, "My Book!"); }); @@ -52,7 +52,7 @@ test("update must handle 500 errors for failed attempts", function (t) { "" ]); t.equal(store.find("products").length, 0); - store.update("products", "12", {}, function () { + store.update("products", "12", {}).subscribe(function () { t.fail("must not call the success callback"); }, function () { t.equal(store.find("products").length, 0); @@ -74,7 +74,7 @@ test("update must use the adapter's 'base' config if present", function (t) { "" ]); store.add({ type: "products", id: "9" }); - store.update("products", "9", {}, function () { + store.update("products", "9", {}).subscribe(function () { t.pass("should call success callback"); }); server.respond(); @@ -88,104 +88,22 @@ test("update must call the error callback if an error is raised during the proce var callback = sinon.spy(); t.plan(1); t.timeoutAfter(1000); - store.define("products", {}); - server.respondWith("PATCH", "/products/1", [ - 204, - { "Content-Type": "application/vnd.api+json" }, - "" - ]); - store.update("products", "1", {}, function () { throw new Error(); }, callback); - server.respond(); - t.equal(callback.callCount, 1); - server.restore(); -}); - -test("update must handle missing success or error callbacks", function (t) { - var server = sinon.fakeServer.create({ autoRespond: false }); - var adapter = new Store.AjaxAdapter(); - var store = new Store(adapter); - var callback = sinon.spy(); - var context = {}; - t.plan(6); - t.timeoutAfter(1000); store.define("products", { - title: Store.attr() - }); - store.add({ - type: "products", - id: "1", - attributes: { - title: "A Book" + foo: { + deserialize: function () { + throw new Error(); + }, + serialize: function () {} } - }) - server.respondWith("PATCH", "/products/1", [ - 204, - { "Content-Type": "application/vnd.api+json" }, - JSON.stringify({ - data: { type: "products", id: "1" } - }) - ]); - server.respondWith("POST", "/products/2", [ - 500, - { "Content-Type": "application/vnd.api+json" }, - "" - ]); - store.update("products", "1", { title: "Something Else" }, null, callback, context); - server.respond(); - t.equal(store.find("products", "1").title, "Something Else"); - t.equal(callback.callCount, 0); - callback.reset(); - store.update("products", "1", { title: "Name" }, callback, context); - server.respond(); - t.equal(callback.callCount, 1); - t.equal(callback.firstCall.thisValue, context); - callback.reset(); - store.update("products", "2", {}, null, callback, context); - server.respond(); - t.equal(callback.callCount, 1); - t.equal(callback.firstCall.thisValue, context); - server.restore(); -}); - -test("update must call callbacks with the context provided", function (t) { - var server = sinon.fakeServer.create({ autoRespond: false }); - var adapter = new Store.AjaxAdapter(); - var store = new Store(adapter); - var callback = sinon.spy(); - var context = {}; - t.plan(4); - t.timeoutAfter(1000); - store.define("products", { - title: Store.attr() }); - store.add({ - type: "products", - id: "1", - attributes: { - title: "A Book" - } - }) server.respondWith("PATCH", "/products/1", [ 204, { "Content-Type": "application/vnd.api+json" }, - JSON.stringify({ - data: { type: "products", id: "1" } - }) - ]); - server.respondWith("POST", "/products/2", [ - 500, - { "Content-Type": "application/vnd.api+json" }, "" ]); - store.update("products", "1", { title: "Something" }, callback, function () {}, context); - server.respond(); - t.equal(callback.callCount, 1); - t.equal(callback.firstCall.thisValue, context); - callback.reset(); - store.update("products", "2", { title: "Something Else" }, function () {}, callback, context); + store.update("products", "1", {}).subscribe(function () {}, callback); server.respond(); t.equal(callback.callCount, 1); - t.equal(callback.firstCall.thisValue, context); server.restore(); }); diff --git a/spec/store/clud/create-spec.js b/spec/store/clud/create-spec.js index 000e48e..3a343b6 100644 --- a/spec/store/clud/create-spec.js +++ b/spec/store/clud/create-spec.js @@ -11,17 +11,24 @@ test("create must throw an error if it is called when there isn't an adapter", f }); test("create must call the create method prodvided by the adapter", function (t) { - var adatper = { create: sinon.spy() }; + var a = {}; + var adatper = { + create: sinon.spy(function () { + return a; + }) + }; var store = new Store(adatper); var type = "foo"; var partial = {}; var options = {}; - var success = function () {}; - var error = function () {}; - var context = {}; - t.plan(2); + var result; + t.plan(6); t.doesNotThrow(function () { - store.create(type, partial, options, success, error, context); + result = store.create(type, partial, options); }, "should not throw an error"); - t.ok(adatper.create.calledWith(store, type, partial, options, success, error, context), "should call adapter with the same params"); + t.equal(adatper.create.lastCall.args[0], store); + t.equal(adatper.create.lastCall.args[1], type); + t.equal(adatper.create.lastCall.args[2], partial); + t.equal(adatper.create.lastCall.args[3], options); + t.equal(result, a); }); diff --git a/spec/store/clud/destroy-spec.js b/spec/store/clud/destroy-spec.js index 85c4754..c650d10 100644 --- a/spec/store/clud/destroy-spec.js +++ b/spec/store/clud/destroy-spec.js @@ -11,17 +11,24 @@ test("destroy must throw an error if it is called when there isn't an adapter", }); test("destroy must call the destroy method prodvided by the adapter", function (t) { - var adatper = { destroy: sinon.spy() }; + var a = {}; + var adatper = { + destroy: sinon.spy(function () { + return a; + }) + }; var store = new Store(adatper); var type = "foo"; var id = "1"; var options = {}; - var success = function () {}; - var error = function () {}; - var context = {}; - t.plan(2); + var result; + t.plan(6); t.doesNotThrow(function () { - store.destroy(type, id, options, success, error, context); + result = store.destroy(type, id, options); }, "should not throw an error"); - t.ok(adatper.destroy.calledWith(store, type, id, options, success, error, context), "should call adapter with the same params"); + t.equal(adatper.destroy.lastCall.args[0], store); + t.equal(adatper.destroy.lastCall.args[1], type); + t.equal(adatper.destroy.lastCall.args[2], id); + t.equal(adatper.destroy.lastCall.args[3], options); + t.equal(result, a); }); diff --git a/spec/store/clud/load-spec.js b/spec/store/clud/load-spec.js index 0ececdd..916370f 100644 --- a/spec/store/clud/load-spec.js +++ b/spec/store/clud/load-spec.js @@ -11,17 +11,24 @@ test("load must throw an error if it is called when there isn't an adapter", fun }); test("load must call the load method prodvided by the adapter", function (t) { - var adatper = { load: sinon.spy() }; + var a = {}; + var adatper = { + load: sinon.spy(function () { + return a; + }) + }; var store = new Store(adatper); var type = "foo"; var id = "1"; var options = {}; - var success = function () {}; - var error = function () {}; - var context = {}; - t.plan(2); + var result; + t.plan(6); t.doesNotThrow(function () { - store.load(type, id, options, success, error, context); + result = store.load(type, id, options); }, "should not throw an error"); - t.ok(adatper.load.calledWith(store, type, id, options, success, error, context), "should call adapter with the same params"); + t.equal(adatper.load.lastCall.args[0], store); + t.equal(adatper.load.lastCall.args[1], type); + t.equal(adatper.load.lastCall.args[2], id); + t.equal(adatper.load.lastCall.args[3], options); + t.equal(result, a); }); diff --git a/spec/store/clud/update-spec.js b/spec/store/clud/update-spec.js index 077d18f..7fca705 100644 --- a/spec/store/clud/update-spec.js +++ b/spec/store/clud/update-spec.js @@ -11,18 +11,26 @@ test("update must throw an error if update is called when there isn't an adapter }); test("update must call the update method prodvided by the adapter", function (t) { - var adatper = { update: sinon.spy() }; + var a = {}; + var adatper = { + update: sinon.spy(function () { + return a; + }) + }; var store = new Store(adatper); var type = "foo"; var id = "1"; var partial = {}; var options = {}; - var success = function () {}; - var error = function () {}; - var context = {}; - t.plan(2); + var result; + t.plan(7); t.doesNotThrow(function () { - store.update(type, id, partial, options, success, error, context); + result = store.update(type, id, partial, options); }, "should not throw an error"); - t.ok(adatper.update.calledWith(store, type, id, partial, options, success, error, context), "should call adapter with the same params"); + t.equal(adatper.update.lastCall.args[0], store); + t.equal(adatper.update.lastCall.args[1], type); + t.equal(adatper.update.lastCall.args[2], id); + t.equal(adatper.update.lastCall.args[3], partial); + t.equal(adatper.update.lastCall.args[4], options); + t.equal(result, a); }); diff --git a/src/ajax-adapter.js b/src/ajax-adapter.js index 3409704..4afb9f1 100644 --- a/src/ajax-adapter.js +++ b/src/ajax-adapter.js @@ -1,196 +1,143 @@ +import ajax from "./ajax"; + export default class AjaxAdapter { constructor(options) { this._base = (options && options.base) || ""; }; - create(store, type, partial, success, error, context) { - - if (error && {}.toString.call(error) !== '[object Function]') { - - this.create(store, type, partial, success, null, error); - - } else if (store._types[type]) { - - let request = new XMLHttpRequest(); - - request.open('POST', `${this._base}/${type}`, true); - - request.setRequestHeader("Content-Type", "application/vnd.api+json"); - - request.onload = function () { - if (request.status >= 200 && request.status < 300) { - let response = JSON.parse(request.responseText); - try { - store.push(response); - if (success) { - success.call(context, store.find(response.data.type, response.data.id)); - } - } catch (e) { - if (error) { - error.call(context, e); - } - } - } else { - if (error) { - error.call(context); - } - } - }; - - request.send(JSON.stringify({ - data: store.convert(type, partial) - })); + create(store, type, partial) { - } else { + if (!store._types[type]) { throw new Error(`Unknown type '${type}'`); } - } - - destroy(store, type, id, success, error, context) { - - if (error && {}.toString.call(error) !== '[object Function]') { - - this.destroy(store, type, id, success, null, error); - - } else if (store._types[type]) { - - let request = new XMLHttpRequest(); - - request.open('DELETE', `${this._base}/${type}/${id}`, true); - - request.setRequestHeader("Content-Type", "application/vnd.api+json"); + let source = ajax({ + body: JSON.stringify({ + data: store.convert(type, partial) + }), + crossDomain: true, + headers: { + "Content-Type": "application/vnd.api+json" + }, + method: "POST", + responseType: "auto", + url: this._getUrl(type) + }).do(event => store.push(event.response)).map(event => { + return store.find(event.response.data.type, event.response.data.id); + }).publish(); + + source.connect(); + + return source; - request.onload = function () { - if (request.status >= 200 && request.status < 300) { - try { - store.remove(type, id); - if (success) { - success.call(context); - } - } catch (e) { - error.call(context, e); - } - } else if (error) { - error.call(context); - } - }; + } - request.send(); + destroy(store, type, id) { - } else { + if (!store._types[type]) { throw new Error(`Unknown type '${type}'`); } - } - - load(store, type, id, options, success, error, context) { - - if (id && {}.toString.call(id) === '[object Function]') { - this.load(store, type, null, null, id, options, success); - } else if (id && typeof id === "object") { - this.load(store, type, null, id, options, success, error); - } else if (options && {}.toString.call(options) === '[object Function]') { - this.load(store, type, id, {}, options, success, error); - } else if (error && {}.toString.call(error) !== '[object Function]') { - this.load(store, type, id, options, success, null, error); - } else if (store._types[type]) { + let source = ajax({ + crossDomain: true, + headers: { + "Content-Type": "application/vnd.api+json" + }, + method: "DELETE", + responseType: "auto", + url: this._getUrl(type, id) + }).do(() => store.remove(type, id)).publish(); - let request = new XMLHttpRequest(); - let url; + source.connect(); - options = options || {}; - url = id ? `${this._base}/${type}/${id}` : `${this._base}/${type}`; + return source; - if (options) { - - let params = []; + } - if (options.fields) { - Object.keys(options.fields).forEach(field => { - options[`fields[${field}]`] = options.fields[field]; - }); - delete options.fields; - } + load(store, type, id, options) { - params = Object.keys(options).map(key => { - return key + "=" + encodeURIComponent(options[key]); - }).sort(); + if (id && typeof id === "object") { + return this.load(store, type, null, id); + } - if (params.length) { - url = `${url}?${params.join("&")}`; - } + if (!store._types[type]) { + throw new Error(`Unknown type '${type}'`); + } - } + let source = ajax({ + crossDomain: true, + headers: { + "Content-Type": "application/vnd.api+json" + }, + method: "GET", + responseType: "auto", + url: this._getUrl(type, id, options) + }).do(event => store.push(event.response)).map(() => { + return id ? store.find(type, id) : store.find(type); + }).publish(); - request.open('GET', url, true); + source.connect(); - request.setRequestHeader("Content-Type", "application/vnd.api+json"); + return source; - request.onload = function () { - if (request.status >= 200 && request.status < 300) { - try { - store.push(JSON.parse(request.responseText)); - if (success) { - success.call(context, id ? store.find(type, id) : store.find(type)); - } - } catch (e) { - error.call({}, e); - } - } else if (error) { - error.call(context); - } - }; + } - request.send(); + update(store, type, id, partial) { - } else { + if (!store._types[type]) { throw new Error(`Unknown type '${type}'`); } - } - - update(store, type, id, partial, success, error, context) { + let data = store.convert(type, id, partial); - if (error && {}.toString.call(error) !== '[object Function]') { + let source = ajax({ + body: JSON.stringify({ + data: data + }), + crossDomain: true, + headers: { + "Content-Type": "application/vnd.api+json" + }, + method: "PATCH", + responseType: "auto", + url: this._getUrl(type, id) + }).do(() => store.add(data)) + .map(() => store.find(type, id)) + .publish(); + + source.connect(); + + return source; - this.update(store, type, id, partial, success, null, error); + } - } else if (store._types[type]) { + _getUrl(type, id, options) { - let request = new XMLHttpRequest(); - let data = store.convert(type, id, partial); + let params = []; + let url = id ? `${this._base}/${type}/${id}` : `${this._base}/${type}`; - request.open('PATCH', `${this._base}/${type}/${id}`, true); + if (options) { - request.setRequestHeader("Content-Type", "application/vnd.api+json"); + if (options.fields) { + Object.keys(options.fields).forEach(field => { + options[`fields[${field}]`] = options.fields[field]; + }); + delete options.fields; + } - request.onload = function () { - if (request.status >= 200 && request.status < 300) { - try { - store.add(data); - if (success) { - success.call(context, store.find(data.type, data.id)); - } - } catch (e) { - if (error) { - error.call(context, e); - } - } - } else if (error) { - error.call(context); - } - }; + params = Object.keys(options).map(key => { + return key + "=" + encodeURIComponent(options[key]); + }).sort(); - request.send(JSON.stringify({ - data: data - })); + if (params.length) { + url = `${url}?${params.join("&")}`; + } - } else { - throw new Error(`Unknown type '${type}'`); } + return url; + } } diff --git a/src/ajax.js b/src/ajax.js new file mode 100644 index 0000000..e20bea3 --- /dev/null +++ b/src/ajax.js @@ -0,0 +1,225 @@ +import Rx from "rx"; + +/** + * + * Derived from RxJS-DOM, Copyright (c) Microsoft Open Technologies, Inc: + * + * https://github.com/Reactive-Extensions/RxJS-DOM + * + * The original source file can be viewed here: + * + * https://github.com/Reactive-Extensions/RxJS-DOM/blob/fdb169c8bd1612318d530e6e54074b1c9e537906/src/ajax.js + * + * Licensed under the Apache License, Version 2.0: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Modifications from original: + * + * - extracted from "Rx.DOM" namespace + * - minor eslinter cleanup ("var" to "let" etc) + * - addition of "auto" responseType + * + */ + +var root = window; + +// Gets the proper XMLHttpRequest for support for older IE +function getXMLHttpRequest() { + if (root.XMLHttpRequest) { + return new root.XMLHttpRequest(); + } else { + let progId; + try { + let progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0']; + for(let i = 0; i < 3; i++) { + try { + progId = progIds[i]; + if (new root.ActiveXObject(progId)) { + break; + } + } catch(e) { } + } + return new root.ActiveXObject(progId); + } catch (e) { + throw new Error('XMLHttpRequest is not supported by your browser'); + } + } +} + +// Get CORS support even for older IE +function getCORSRequest() { + var xhr = new root.XMLHttpRequest(); + if ('withCredentials' in xhr) { + return xhr; + } else if (!!root.XDomainRequest) { + return new XDomainRequest(); + } else { + throw new Error('CORS is not supported by your browser'); + } +} + +function normalizeAjaxSuccessEvent(e, xhr, settings) { + var response = ('response' in xhr) ? xhr.response : xhr.responseText; + if (settings.responseType === 'auto') { + try { + response = JSON.parse(response); + } catch (e) {} + } else { + response = settings.responseType === 'json' ? JSON.parse(response) : response; + } + return { + response: response, + status: xhr.status, + responseType: xhr.responseType, + xhr: xhr, + originalEvent: e + }; +} + +function normalizeAjaxErrorEvent(e, xhr, type) { + return { + type: type, + status: xhr.status, + xhr: xhr, + originalEvent: e + }; +} + +/** + * Creates an observable for an Ajax request with either a settings object with url, headers, etc or a string for a URL. + * + * @example + * source = ajax('/products'); + * source = ajax({ url: 'products', method: 'GET' }); + * + * @param {Object} settings Can be one of the following: + * + * A string of the URL to make the Ajax call. + * An object with the following properties + * - url: URL of the request + * - body: The body of the request + * - method: Method of the request, such as GET, POST, PUT, PATCH, DELETE + * - async: Whether the request is async + * - headers: Optional headers + * - crossDomain: true if a cross domain request, else false + * - responseType: "text" (default), "json" or "auto" + * + * @returns {Observable} An observable sequence containing the XMLHttpRequest. +*/ +export default function (options) { + var settings = { + method: 'GET', + crossDomain: false, + async: true, + headers: {}, + responseType: 'text', + createXHR: function(){ + return this.crossDomain ? getCORSRequest() : getXMLHttpRequest(); + }, + normalizeError: normalizeAjaxErrorEvent, + normalizeSuccess: normalizeAjaxSuccessEvent + }; + + if(typeof options === 'string') { + settings.url = options; + } else { + for(let prop in options) { + if(hasOwnProperty.call(options, prop)) { + settings[prop] = options[prop]; + } + } + } + + let normalizeError = settings.normalizeError; + let normalizeSuccess = settings.normalizeSuccess; + + if (!settings.crossDomain && !settings.headers['X-Requested-With']) { + settings.headers['X-Requested-With'] = 'XMLHttpRequest'; + } + settings.hasContent = settings.body !== undefined; + + return new Rx.AnonymousObservable(function (observer) { + var isDone = false; + var xhr; + + var processResponse = function(xhr, e){ + var status = xhr.status === 1223 ? 204 : xhr.status; + if ((status >= 200 && status <= 300) || status === 0 || status === '') { + observer.onNext(normalizeSuccess(e, xhr, settings)); + observer.onCompleted(); + } else { + observer.onError(normalizeError(e, xhr, 'error')); + } + isDone = true; + }; + + try { + xhr = settings.createXHR();; + } catch (err) { + observer.onError(err); + } + + try { + if (settings.user) { + xhr.open(settings.method, settings.url, settings.async, settings.user, settings.password); + } else { + xhr.open(settings.method, settings.url, settings.async); + } + + let headers = settings.headers; + for (let header in headers) { + if (hasOwnProperty.call(headers, header)) { + xhr.setRequestHeader(header, headers[header]); + } + } + + if(!!xhr.upload || (!('withCredentials' in xhr) && !!root.XDomainRequest)) { + xhr.onload = function(e) { + if(settings.progressObserver) { + settings.progressObserver.onNext(e); + settings.progressObserver.onCompleted(); + } + processResponse(xhr, e); + }; + + if(settings.progressObserver) { + xhr.onprogress = function(e) { + settings.progressObserver.onNext(e); + }; + } + + xhr.onerror = function(e) { + if (settings.progressObserver) { + settings.progressObserver.onError(e); + } + observer.onError(normalizeError(e, xhr, 'error')); + isDone = true; + }; + + xhr.onabort = function(e) { + if (settings.progressObserver) { + settings.progressObserver.onError(e); + } + observer.onError(normalizeError(e, xhr, 'abort')); + isDone = true; + }; + } else { + + xhr.onreadystatechange = function (e) { + if (xhr.readyState === 4) { + processResponse(xhr, e); + } + }; + } + + xhr.send(settings.hasContent && settings.body || null); + } catch (e) { + observer.onError(e); + } + + return function () { + if (!isDone && xhr.readyState !== 4) { xhr.abort(); } + }; + }); +}; diff --git a/src/store.js b/src/store.js index b97e27a..4d6936f 100644 --- a/src/store.js +++ b/src/store.js @@ -1,5 +1,5 @@ import "array.prototype.find"; -import Rx from "rx-lite"; +import Rx from "rx"; import AjaxAdapter from "./ajax-adapter"; export default class Store { @@ -238,20 +238,18 @@ export default class Store { * @param {!string} type - Type of resource. * @param {!Object} partial - Data to create the resource with. * @param {Object} [options] - Options to pass to the adapter. - * @param {function} [success] - Callback on success. - * @param {function} [error] - Callback on error. - * @param {Object} [context] - Context for the callbacks. + * @return {Rx.Observable} * * @example * let adapter = new Store.AjaxAdapter(); * let store = new Store(adpater); - * store.create("product", { title: "A Book" }, (product) => { + * store.create("product", { title: "A Book" }).subscribe((product) => { * console.log(product.title); * }); */ - create(type, partial, options, success, error, context) { + create(type, partial, options) { if (this._adapter) { - this._adapter.create(this, type, partial, options, success, error, context); + return this._adapter.create(this, type, partial, options); } else { throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); } @@ -288,20 +286,18 @@ export default class Store { * @param {!string} type - Type of resource. * @param {!string} id - ID of resource. * @param {Object} [options] - Options to pass to the adapter. - * @param {function} [success] - Callback on success. - * @param {function} [error] - Callback on error. - * @param {Object} [context] - Context for the callbacks. + * @return {Rx.Observable} * * @example * let adapter = new Store.AjaxAdapter(); * let store = new Store(adpater); - * store.destroy("product", "1", () => { + * store.destroy("product", "1").subscribe(() => { * console.log("Destroyed!"); * }); */ - destroy(type, id, options, success, error, context) { + destroy(type, id, options) { if (this._adapter) { - this._adapter.destroy(this, type, id, options, success, error, context); + return this._adapter.destroy(this, type, id, options); } else { throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); } @@ -360,20 +356,18 @@ export default class Store { * @param {!string} type - Type of resource. * @param {!string} [id] - ID of resource. * @param {Object} [options] - Options to pass to the adapter. - * @param {function} [success] - Callback on success. - * @param {function} [error] - Callback on error. - * @param {Object} [context] - Context for the callbacks. + * @return {Rx.Observable} * * @example * let adapter = new Store.AjaxAdapter(); * let store = new Store(adpater); - * store.load("product", "1", (product) => { + * store.load("product", "1").subscribe((product) => { * console.log(product.title); * }); */ - load(type, id, options, success, error, context) { + load(type, id, options) { if (this._adapter) { - this._adapter.load(this, type, id, options, success, error, context); + return this._adapter.load(this, type, id, options); } else { throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); } @@ -515,20 +509,18 @@ export default class Store { * @param {!string} id - ID of resource. * @param {!Object} partial - Data to update the resource with. * @param {Object} [options] - Options to pass to the adapter. - * @param {function} [success] - Callback on success. - * @param {function} [error] - Callback on error. - * @param {Object} [context] - Context for the callbacks. + * @return {Rx.Observable} * * @example * let adapter = new Store.AjaxAdapter(); * let store = new Store(adpater); - * store.update("product", "1", { title: "foo" }, (product) => { + * store.update("product", "1", { title: "foo" }).subscribe((product) => { * console.log(product.title); * }); */ - update(type, id, partial, options, success, error, context) { + update(type, id, partial, options) { if (this._adapter) { - this._adapter.update(this, type, id, partial, options, success, error, context); + return this._adapter.update(this, type, id, partial, options); } else { throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); }