From be6385426fe407bc929241175daba6b590cd32b2 Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Wed, 26 Aug 2015 15:15:20 +0930 Subject: [PATCH 01/23] Sub-out ajax adapter specs. --- spec/{store => adapters}/ajax/create-spec.js | 5 +++-- spec/{store => adapters}/ajax/destroy-spec.js | 5 +++-- spec/adapters/ajax/load-spec.js | 12 ++++++++++++ spec/{store => adapters}/ajax/update-spec.js | 5 +++-- spec/store/ajax/read-spec.js | 11 ----------- 5 files changed, 21 insertions(+), 17 deletions(-) rename spec/{store => adapters}/ajax/create-spec.js (55%) rename spec/{store => adapters}/ajax/destroy-spec.js (55%) create mode 100644 spec/adapters/ajax/load-spec.js rename spec/{store => adapters}/ajax/update-spec.js (55%) delete mode 100644 spec/store/ajax/read-spec.js diff --git a/spec/store/ajax/create-spec.js b/spec/adapters/ajax/create-spec.js similarity index 55% rename from spec/store/ajax/create-spec.js rename to spec/adapters/ajax/create-spec.js index 8f4b40e..d84e606 100644 --- a/spec/store/ajax/create-spec.js +++ b/spec/adapters/ajax/create-spec.js @@ -2,10 +2,11 @@ var Store = require("../../../src/store"); describe("create", function() { - var store; + var adapter, store; beforeEach(function() { - store = new Store(); + adapter = new Store.AjaxAdapter(); + store = new Store(adapter); }); }); diff --git a/spec/store/ajax/destroy-spec.js b/spec/adapters/ajax/destroy-spec.js similarity index 55% rename from spec/store/ajax/destroy-spec.js rename to spec/adapters/ajax/destroy-spec.js index 25392bf..61646b8 100644 --- a/spec/store/ajax/destroy-spec.js +++ b/spec/adapters/ajax/destroy-spec.js @@ -2,10 +2,11 @@ var Store = require("../../../src/store"); describe("destroy", function() { - var store; + var adapter, store; beforeEach(function() { - store = new Store(); + adapter = new Store.AjaxAdapter(); + store = new Store(adapter); }); }); diff --git a/spec/adapters/ajax/load-spec.js b/spec/adapters/ajax/load-spec.js new file mode 100644 index 0000000..ab46db3 --- /dev/null +++ b/spec/adapters/ajax/load-spec.js @@ -0,0 +1,12 @@ +var Store = require("../../../src/store"); + +describe("load", function() { + + var adapter, store; + + beforeEach(function() { + adapter = new Store.AjaxAdapter(); + store = new Store(adapter); + }); + +}); diff --git a/spec/store/ajax/update-spec.js b/spec/adapters/ajax/update-spec.js similarity index 55% rename from spec/store/ajax/update-spec.js rename to spec/adapters/ajax/update-spec.js index a7f1b0f..029a4a5 100644 --- a/spec/store/ajax/update-spec.js +++ b/spec/adapters/ajax/update-spec.js @@ -2,10 +2,11 @@ var Store = require("../../../src/store"); describe("update", function() { - var store; + var adapter, store; beforeEach(function() { - store = new Store(); + adapter = new Store.AjaxAdapter(); + store = new Store(adapter); }); }); diff --git a/spec/store/ajax/read-spec.js b/spec/store/ajax/read-spec.js deleted file mode 100644 index f926da6..0000000 --- a/spec/store/ajax/read-spec.js +++ /dev/null @@ -1,11 +0,0 @@ -var Store = require("../../../src/store"); - -describe("read", function() { - - var store; - - beforeEach(function() { - store = new Store(); - }); - -}); -- 2.51.2 From 954305546677cb84b0d63a6caddd0677eae1ad4f Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Thu, 27 Aug 2015 09:52:30 +0930 Subject: [PATCH 02/23] Start adding the CLUD methods. --- spec/store/store-spec.js | 30 ++++++++++++++++++++++++++++++ src/store.js | 16 ++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 spec/store/store-spec.js diff --git a/spec/store/store-spec.js b/spec/store/store-spec.js new file mode 100644 index 0000000..542f29e --- /dev/null +++ b/spec/store/store-spec.js @@ -0,0 +1,30 @@ +var Store = require("../../src/store"); + +describe("store", function () { + + var store; + + beforeEach(function () { + store = new Store(); + }); + + describe("without an adapter", function () { + + it("must throw an error if CLUD methods are called when there isn't an adapter", function () { + expect(function () { + store.create(); + }).toThrowError("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + expect(function () { + store.load(); + }).toThrowError("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + expect(function () { + store.update(); + }).toThrowError("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + expect(function () { + store.destroy(); + }).toThrowError("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + }); + + }); + +}); diff --git a/src/store.js b/src/store.js index 29f217c..a441edc 100644 --- a/src/store.js +++ b/src/store.js @@ -129,6 +129,10 @@ export default class Store { } } + create() { + throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + } + /** * Defines a type of resource. * @@ -149,6 +153,10 @@ export default class Store { }); } + destroy() { + throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + } + /** * Find a resource or entire collection of resources. * @@ -194,6 +202,10 @@ export default class Store { } } + load() { + throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + } + /** * Unregister an event listener that was registered with on(). * @@ -328,6 +340,10 @@ export default class Store { } } + update() { + throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + } + _addField(object, resource, definition, fieldName) { var field = definition[fieldName]; var newValue = field.deserialize.call(this, object, fieldName); -- 2.51.2 From bfaa60daa7dbb5e14f40e8058cd93ae64a8327c0 Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Thu, 27 Aug 2015 13:15:17 +0930 Subject: [PATCH 03/23] Reorganise the tests. --- package.json | 2 +- spec/helpers/matchers.js | 25 ------------------------- spec/store/clud/create-spec.js | 21 +++++++++++++++++++++ spec/store/clud/destroy-spec.js | 21 +++++++++++++++++++++ spec/store/clud/load-spec.js | 21 +++++++++++++++++++++ spec/store/clud/update-spec.js | 21 +++++++++++++++++++++ spec/store/core/add-spec.js | 14 +++++++------- spec/store/core/remove-spec.js | 4 ++-- spec/store/store-spec.js | 30 ------------------------------ 9 files changed, 94 insertions(+), 65 deletions(-) delete mode 100644 spec/helpers/matchers.js create mode 100644 spec/store/clud/create-spec.js create mode 100644 spec/store/clud/destroy-spec.js create mode 100644 spec/store/clud/load-spec.js create mode 100644 spec/store/clud/update-spec.js delete mode 100644 spec/store/store-spec.js diff --git a/package.json b/package.json index eed276d..37a51d0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "repository": "haydn/json-api-store", "main": "dist/store.js", "scripts": { - "test": "node_modules/jasmine/bin/jasmine.js" + "test": "./node_modules/.bin/jasmine" }, "author": "Haydn Ewers", "license": "MIT", diff --git a/spec/helpers/matchers.js b/spec/helpers/matchers.js deleted file mode 100644 index f7eb89e..0000000 --- a/spec/helpers/matchers.js +++ /dev/null @@ -1,25 +0,0 @@ -beforeEach(function () { - - jasmine.addMatchers({ - - toHaveIds: function (util, customEqualityTesters) { - customEqualityTesters = customEqualityTesters || []; - return { - compare: function (actual, expected) { - var result = { pass: false }; - actual = actual.map(function (resource) { - return resource.id; - }).sort(); - expected = expected.sort(); - result.pass = util.equals(actual, expected, customEqualityTesters); - if (!result.pass) { - result.message = "Expected " + JSON.stringify(actual) + " to have ids " + JSON.stringify(expected) + "."; - } - return result; - } - }; - } - - }); - -}); diff --git a/spec/store/clud/create-spec.js b/spec/store/clud/create-spec.js new file mode 100644 index 0000000..1211f5e --- /dev/null +++ b/spec/store/clud/create-spec.js @@ -0,0 +1,21 @@ +var Store = require("../../../src/store"); + +describe("store", function () { + + var store; + + beforeEach(function () { + store = new Store(); + }); + + describe("without an adapter", function () { + + it("must throw an error if destroy is called when there isn't an adapter", function () { + expect(function () { + store.create(); + }).toThrowError("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + }); + + }); + +}); diff --git a/spec/store/clud/destroy-spec.js b/spec/store/clud/destroy-spec.js new file mode 100644 index 0000000..b863e40 --- /dev/null +++ b/spec/store/clud/destroy-spec.js @@ -0,0 +1,21 @@ +var Store = require("../../../src/store"); + +describe("store", function () { + + var store; + + beforeEach(function () { + store = new Store(); + }); + + describe("without an adapter", function () { + + it("must throw an error if destroy is called when there isn't an adapter", function () { + expect(function () { + store.destroy(); + }).toThrowError("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + }); + + }); + +}); diff --git a/spec/store/clud/load-spec.js b/spec/store/clud/load-spec.js new file mode 100644 index 0000000..7933f61 --- /dev/null +++ b/spec/store/clud/load-spec.js @@ -0,0 +1,21 @@ +var Store = require("../../../src/store"); + +describe("store", function () { + + var store; + + beforeEach(function () { + store = new Store(); + }); + + describe("without an adapter", function () { + + it("must throw an error if load is called when there isn't an adapter", function () { + expect(function () { + store.load(); + }).toThrowError("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + }); + + }); + +}); diff --git a/spec/store/clud/update-spec.js b/spec/store/clud/update-spec.js new file mode 100644 index 0000000..246ffe9 --- /dev/null +++ b/spec/store/clud/update-spec.js @@ -0,0 +1,21 @@ +var Store = require("../../../src/store"); + +describe("store", function () { + + var store; + + beforeEach(function () { + store = new Store(); + }); + + describe("without an adapter", function () { + + it("must throw an error if update is called when there isn't an adapter", function () { + expect(function () { + store.update(); + }).toThrowError("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + }); + + }); + +}); diff --git a/spec/store/core/add-spec.js b/spec/store/core/add-spec.js index 5591f54..0800495 100644 --- a/spec/store/core/add-spec.js +++ b/spec/store/core/add-spec.js @@ -1,10 +1,10 @@ var Store = require("../../../src/store"); -describe("add", function() { +describe("add", function () { var store; - beforeEach(function() { + beforeEach(function () { store = new Store(); }); @@ -167,7 +167,7 @@ describe("add", function() { } } }); - expect(store.find("categories", "1").products).toHaveIds([ "10", "20" ]); + expect(store.find("categories", "1").products.map(x => x.id).sort()).toEqual([ "10", "20" ]); }); it("must setup inverse many-to-many relationships", function () { @@ -199,7 +199,7 @@ describe("add", function() { } } }); - expect(store.find("products", "1").categories).toHaveIds([ "10", "20" ]); + expect(store.find("products", "1").categories.map(x => x.id).sort()).toEqual([ "10", "20" ]); }); it("must setup inverse many-to-one relationships", function () { @@ -350,7 +350,7 @@ describe("add", function() { } } }); - expect(store.find("categories", "34").products).toHaveIds([ "44" ]); + expect(store.find("categories", "34").products.map(x => x.id).sort()).toEqual([ "44" ]); store.add({ "type": "products", "id": "44", @@ -416,8 +416,8 @@ describe("add", function() { } } }); - expect(store.find("categories", "37").products).toHaveIds([ "23" ]); - expect(store.find("products", "23").categories).toHaveIds([ "37" ]); + expect(store.find("categories", "37").products.map(x => x.id).sort()).toEqual([ "23" ]); + expect(store.find("products", "23").categories.map(x => x.id).sort()).toEqual([ "37" ]); }); }); diff --git a/spec/store/core/remove-spec.js b/spec/store/core/remove-spec.js index 4dcc759..ff3a85b 100644 --- a/spec/store/core/remove-spec.js +++ b/spec/store/core/remove-spec.js @@ -68,9 +68,9 @@ describe("remove", function() { } } }); - expect(store.find("categories", "1").products).toHaveIds([ "10", "11" ]); + expect(store.find("categories", "1").products.map(x => x.id).sort()).toEqual([ "10", "11" ]); store.remove("products", "10"); - expect(store.find("categories", "1").products).toHaveIds([ "11" ]); + expect(store.find("categories", "1").products.map(x => x.id).sort()).toEqual([ "11" ]); store.remove("categories", "1"); expect(store.find("products", "11").category).toBe(null); }); diff --git a/spec/store/store-spec.js b/spec/store/store-spec.js deleted file mode 100644 index 542f29e..0000000 --- a/spec/store/store-spec.js +++ /dev/null @@ -1,30 +0,0 @@ -var Store = require("../../src/store"); - -describe("store", function () { - - var store; - - beforeEach(function () { - store = new Store(); - }); - - describe("without an adapter", function () { - - it("must throw an error if CLUD methods are called when there isn't an adapter", function () { - expect(function () { - store.create(); - }).toThrowError("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); - expect(function () { - store.load(); - }).toThrowError("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); - expect(function () { - store.update(); - }).toThrowError("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); - expect(function () { - store.destroy(); - }).toThrowError("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); - }); - - }); - -}); -- 2.51.2 From 999c4d40642877c33a7a34b831a1675e1102cd41 Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Thu, 27 Aug 2015 16:26:53 +0930 Subject: [PATCH 04/23] Change the test framework from Jasmine to Tape. --- .gitignore | 1 + package.json | 13 +- spec/.eslintrc | 9 +- spec/adapters/ajax/create-spec.js | 14 +- spec/adapters/ajax/destroy-spec.js | 14 +- spec/adapters/ajax/load-spec.js | 14 +- spec/adapters/ajax/update-spec.js | 14 +- spec/client-test.js | 5 + spec/common-test.js | 14 + spec/helpers/babel.js | 1 - spec/server-test.js | 1 + spec/store/clud/create-spec.js | 29 +- spec/store/clud/destroy-spec.js | 29 +- spec/store/clud/load-spec.js | 29 +- spec/store/clud/update-spec.js | 29 +- spec/store/core/add-spec.js | 769 +++++++++++++++-------------- spec/store/core/define-spec.js | 67 ++- spec/store/core/find-spec.js | 91 ++-- spec/store/core/push-spec.js | 118 +++-- spec/store/core/remove-spec.js | 134 ++--- spec/store/events/off-spec.js | 135 ++--- spec/store/events/on-spec.js | 340 +++++++------ spec/store/fields/attr-spec.js | 100 ++-- spec/store/fields/has-many-spec.js | 282 +++++------ spec/store/fields/has-one-spec.js | 221 +++++---- spec/support/jasmine.json | 9 - 26 files changed, 1254 insertions(+), 1228 deletions(-) create mode 100644 spec/client-test.js create mode 100644 spec/common-test.js delete mode 100644 spec/helpers/babel.js create mode 100644 spec/server-test.js delete mode 100644 spec/support/jasmine.json diff --git a/.gitignore b/.gitignore index 9daa824..7595163 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store node_modules +npm-debug.log diff --git a/package.json b/package.json index 37a51d0..dcbd77e 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,22 @@ "repository": "haydn/json-api-store", "main": "dist/store.js", "scripts": { - "test": "./node_modules/.bin/jasmine" + "test": "npm run test-server && npm run test-client", + "test-pretty": "npm run test-server | node_modules/.bin/tap-spec && npm run test-client | node_modules/.bin/tap-spec || true", + "build": "node_modules/.bin/babel src/store.js -m umd --module-id Store", + "test-server": "node_modules/.bin/babel-node spec/server-test.js", + "test-client": "node_modules/.bin/browserify spec/client-test.js -t babelify | node_modules/.bin/tape-run" }, "author": "Haydn Ewers", "license": "MIT", "devDependencies": { "babel": "^5.8.21", - "jasmine": "^2.3.2" + "babelify": "^6.2.0", + "browserify": "^11.0.1", + "sinon": "^1.16.1", + "tap-spec": "^4.1.0", + "tape": "^4.2.0", + "tape-run": "^1.1.0" }, "dependencies": { "array.prototype.find": "^1.0.0" diff --git a/spec/.eslintrc b/spec/.eslintrc index 9e63ee9..412ae83 100644 --- a/spec/.eslintrc +++ b/spec/.eslintrc @@ -1,6 +1,11 @@ { "env": { - "jasmine": true + "es6": true }, - "rules": {} + "ecmaFeatures": { + "modules": true + }, + "rules": { + "key-spacing": 0 + } } diff --git a/spec/adapters/ajax/create-spec.js b/spec/adapters/ajax/create-spec.js index d84e606..18bc4eb 100644 --- a/spec/adapters/ajax/create-spec.js +++ b/spec/adapters/ajax/create-spec.js @@ -1,12 +1,2 @@ -var Store = require("../../../src/store"); - -describe("create", function() { - - var adapter, store; - - beforeEach(function() { - adapter = new Store.AjaxAdapter(); - store = new Store(adapter); - }); - -}); +// import test from "tape"; +// import Store from "../../../src/store"; diff --git a/spec/adapters/ajax/destroy-spec.js b/spec/adapters/ajax/destroy-spec.js index 61646b8..18bc4eb 100644 --- a/spec/adapters/ajax/destroy-spec.js +++ b/spec/adapters/ajax/destroy-spec.js @@ -1,12 +1,2 @@ -var Store = require("../../../src/store"); - -describe("destroy", function() { - - var adapter, store; - - beforeEach(function() { - adapter = new Store.AjaxAdapter(); - store = new Store(adapter); - }); - -}); +// import test from "tape"; +// import Store from "../../../src/store"; diff --git a/spec/adapters/ajax/load-spec.js b/spec/adapters/ajax/load-spec.js index ab46db3..18bc4eb 100644 --- a/spec/adapters/ajax/load-spec.js +++ b/spec/adapters/ajax/load-spec.js @@ -1,12 +1,2 @@ -var Store = require("../../../src/store"); - -describe("load", function() { - - var adapter, store; - - beforeEach(function() { - adapter = new Store.AjaxAdapter(); - store = new Store(adapter); - }); - -}); +// import test from "tape"; +// import Store from "../../../src/store"; diff --git a/spec/adapters/ajax/update-spec.js b/spec/adapters/ajax/update-spec.js index 029a4a5..18bc4eb 100644 --- a/spec/adapters/ajax/update-spec.js +++ b/spec/adapters/ajax/update-spec.js @@ -1,12 +1,2 @@ -var Store = require("../../../src/store"); - -describe("update", function() { - - var adapter, store; - - beforeEach(function() { - adapter = new Store.AjaxAdapter(); - store = new Store(adapter); - }); - -}); +// import test from "tape"; +// import Store from "../../../src/store"; diff --git a/spec/client-test.js b/spec/client-test.js new file mode 100644 index 0000000..94d7a83 --- /dev/null +++ b/spec/client-test.js @@ -0,0 +1,5 @@ +import "./common-test"; +import "./adapters/ajax/create-spec"; +import "./adapters/ajax/destroy-spec"; +import "./adapters/ajax/load-spec"; +import "./adapters/ajax/update-spec"; diff --git a/spec/common-test.js b/spec/common-test.js new file mode 100644 index 0000000..14e2216 --- /dev/null +++ b/spec/common-test.js @@ -0,0 +1,14 @@ +import "./store/clud/create-spec"; +import "./store/clud/destroy-spec"; +import "./store/clud/load-spec"; +import "./store/clud/update-spec"; +import "./store/core/add-spec"; +import "./store/core/define-spec"; +import "./store/core/find-spec"; +import "./store/core/push-spec"; +import "./store/core/remove-spec"; +import "./store/events/off-spec"; +import "./store/events/on-spec"; +import "./store/fields/attr-spec"; +import "./store/fields/has-many-spec"; +import "./store/fields/has-one-spec"; diff --git a/spec/helpers/babel.js b/spec/helpers/babel.js deleted file mode 100644 index 5477f10..0000000 --- a/spec/helpers/babel.js +++ /dev/null @@ -1 +0,0 @@ -require("babel/register"); diff --git a/spec/server-test.js b/spec/server-test.js new file mode 100644 index 0000000..8e06e99 --- /dev/null +++ b/spec/server-test.js @@ -0,0 +1 @@ +import "./common-test"; diff --git a/spec/store/clud/create-spec.js b/spec/store/clud/create-spec.js index 1211f5e..05c614b 100644 --- a/spec/store/clud/create-spec.js +++ b/spec/store/clud/create-spec.js @@ -1,21 +1,10 @@ -var Store = require("../../../src/store"); - -describe("store", function () { - - var store; - - beforeEach(function () { - store = new Store(); - }); - - describe("without an adapter", function () { - - it("must throw an error if destroy is called when there isn't an adapter", function () { - expect(function () { - store.create(); - }).toThrowError("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); - }); - - }); - +import test from "tape"; +import Store from "../../../src/store"; + +test("create must throw an error if it is called when there isn't an adapter", function (t) { + var store = new Store(); + t.plan(1); + t.throws(function () { + store.create(); + }, /Adapter missing\. Specify an adapter when creating the store: `var store = new Store\(adapter\);`/); }); diff --git a/spec/store/clud/destroy-spec.js b/spec/store/clud/destroy-spec.js index b863e40..3c7eb16 100644 --- a/spec/store/clud/destroy-spec.js +++ b/spec/store/clud/destroy-spec.js @@ -1,21 +1,10 @@ -var Store = require("../../../src/store"); - -describe("store", function () { - - var store; - - beforeEach(function () { - store = new Store(); - }); - - describe("without an adapter", function () { - - it("must throw an error if destroy is called when there isn't an adapter", function () { - expect(function () { - store.destroy(); - }).toThrowError("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); - }); - - }); - +import test from "tape"; +import Store from "../../../src/store"; + +test("destroy must throw an error if it is called when there isn't an adapter", function (t) { + var store = new Store(); + t.plan(1); + t.throws(function () { + store.destroy(); + }, /Adapter missing\. Specify an adapter when creating the store: `var store = new Store\(adapter\);`/); }); diff --git a/spec/store/clud/load-spec.js b/spec/store/clud/load-spec.js index 7933f61..0aac5a7 100644 --- a/spec/store/clud/load-spec.js +++ b/spec/store/clud/load-spec.js @@ -1,21 +1,10 @@ -var Store = require("../../../src/store"); - -describe("store", function () { - - var store; - - beforeEach(function () { - store = new Store(); - }); - - describe("without an adapter", function () { - - it("must throw an error if load is called when there isn't an adapter", function () { - expect(function () { - store.load(); - }).toThrowError("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); - }); - - }); - +import test from "tape"; +import Store from "../../../src/store"; + +test("load must throw an error if it is called when there isn't an adapter", function (t) { + var store = new Store(); + t.plan(1); + t.throws(function () { + store.load(); + }, /Adapter missing\. Specify an adapter when creating the store: `var store = new Store\(adapter\);`/); }); diff --git a/spec/store/clud/update-spec.js b/spec/store/clud/update-spec.js index 246ffe9..d2d2e18 100644 --- a/spec/store/clud/update-spec.js +++ b/spec/store/clud/update-spec.js @@ -1,21 +1,10 @@ -var Store = require("../../../src/store"); - -describe("store", function () { - - var store; - - beforeEach(function () { - store = new Store(); - }); - - describe("without an adapter", function () { - - it("must throw an error if update is called when there isn't an adapter", function () { - expect(function () { - store.update(); - }).toThrowError("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); - }); - - }); - +import test from "tape"; +import Store from "../../../src/store"; + +test("update must throw an error if update is called when there isn't an adapter", function (t) { + var store = new Store(); + t.plan(1); + t.throws(function () { + store.update(); + }, /Adapter missing\. Specify an adapter when creating the store: `var store = new Store\(adapter\);`/); }); diff --git a/spec/store/core/add-spec.js b/spec/store/core/add-spec.js index 0800495..1a79aa1 100644 --- a/spec/store/core/add-spec.js +++ b/spec/store/core/add-spec.js @@ -1,425 +1,448 @@ -var Store = require("../../../src/store"); +import test from "tape"; +import Store from "../../../src/store"; -describe("add", function () { +test("add must add a resource to the store", function (t) { + var store = new Store(); + t.plan(2); + store.define("products", {}); + store.add({ + "type": "products", + "id": "44" + }); + t.equal(store.find("products").length, 1); + t.equal(store.find("products")[0].id, "44"); +}); - var store; +test("add must throw an error if the data doesn't have a type and id", function (t) { + var store = new Store(); + t.plan(1); + t.throws(function () { + store.add({}); + }, /The data must have a type and id/); +}); - beforeEach(function () { - store = new Store(); - }); +test("add must throw an error when called without arguments", function (t) { + var store = new Store(); + t.plan(1); + t.throws(function () { + store.add(); + }, /You must provide data to add/); +}); - it("must add a resource to the store", function () { - store.define("products", {}); +test("add must throw an error if the type has not been defined", function (t) { + var store = new Store(); + t.plan(1); + t.throws(function () { store.add({ "type": "products", "id": "44" }); - expect(store.find("products").length).toBe(1); - expect(store.find("products")[0].id).toBe("44"); + }, /Unknown type 'products'/); +}); + +test("add must use deserialize functions provided by type definitions", function (t) { + var store = new Store(); + t.plan(1); + store.define("products", { + title: { + deserialize: function (data, key) { + return "Example " + data.id + " " + key; + } + } + }); + store.add({ + "type": "products", + "id": "44" }); + t.equal(store.find("products", "44").title, "Example 44 title"); +}); - it("must throw an error if the data doesn't have a type and id", function () { - expect(function () { - store.add({}); - }).toThrowError(TypeError, "The data must have a type and id"); +test("add must not set fields when the deserialize function returns undefined", function (t) { + var store = new Store(); + t.plan(2); + store.define("products", { + title: { + deserialize: function (data) { + if (data.attributes && data.attributes.title) { + return "Example"; + } + } + } + }); + store.add({ + "type": "products", + "id": "44", + "attributes": { + "title": true + } }); + t.equal(store.find("products", "44").title, "Example"); + store.add({ + "type": "products", + "id": "44" + }); + t.equal(store.find("products", "44").title, "Example"); +}); - it("must throw an error when called without arguments", function () { - expect(function () { - store.add(); - }).toThrowError(TypeError, "You must provide data to add"); +test("add must set fields when the deserialize function returns null", function (t) { + var store = new Store(); + t.plan(2); + store.define("products", { + title: { + deserialize: function (data) { + if (data.attributes && data.attributes.title) { + return "Example"; + } else { + return null; + } + } + } }); + store.add({ + "type": "products", + "id": "44", + "attributes": { + "title": true + } + }); + t.equal(store.find("products", "44").title, "Example"); + store.add({ + "type": "products", + "id": "44" + }); + t.equal(store.find("products", "44").title, null); +}); - it("must throw an error if the type has not been defined", function () { - expect(function () { - store.add({ - "type": "products", - "id": "44" - }); - }).toThrowError(TypeError, "Unknown type 'products'"); +test("add must call deserialize functions in the context of the store", function (t) { + var store = new Store(); + t.plan(1); + store.define("products", { + example: { + deserialize: function () { + return this; + } + } }); + store.add({ + "type": "products", + "id": "44" + }); + t.equal(store.find("products", "44").example, store); +}); - it("must use deserialize functions provided by type definitions", function () { - store.define("products", { - title: { - deserialize: function (data, key) { - return "Example " + data.id + " " + key; - } +test("add inverse relationships must setup inverse one-to-one relationships", function (t) { + var store = new Store(); + t.plan(1); + store.define("categories", { + product: Store.hasOne({ inverse: "category" }) + }); + store.define("products", { + category: Store.hasOne({ inverse: "product" }) + }); + store.add({ + "type": "products", + "id": "10", + "relationships": { + "category": { + "data": { "type": "categories", "id": "1" } } - }); - store.add({ - "type": "products", - "id": "44" - }); - expect(store.find("products", "44").title).toBe("Example 44 title"); + } }); + t.equal(store.find("categories", "1").product, store.find("products", "10")); +}); - it("must not set fields when the deserialize function returns undefined", function () { - store.define("products", { - title: { - deserialize: function (data) { - if (data.attributes && data.attributes.title) { - return "Example"; - } - } +test("add inverse relationships must setup inverse one-to-many relationships", function (t) { + var store = new Store(); + t.plan(1); + store.define("categories", { + products: Store.hasMany({ inverse: "category" }) + }); + store.define("products", { + category: Store.hasOne({ inverse: "products" }) + }); + store.add({ + "type": "products", + "id": "10", + "relationships": { + "category": { + "data": { "type": "categories", "id": "1" } } - }); + } + }); + store.add({ + "type": "products", + "id": "20", + "relationships": { + "category": { + "data": { "type": "categories", "id": "1" } + } + } + }); + t.deepEqual(store.find("categories", "1").products.map(x => x.id).sort(), [ "10", "20" ]); +}); + +test("add inverse relationships must setup inverse many-to-many relationships", function (t) { + var store = new Store(); + t.plan(1); + store.define("categories", { + products: Store.hasMany({ inverse: "categories" }) + }); + store.define("products", { + categories: Store.hasMany({ inverse: "products" }) + }); + store.add({ + "type": "categories", + "id": "10", + "relationships": { + "products": { + "data": [ + { "type": "products", "id": "1" } + ] + } + } + }); + store.add({ + "type": "categories", + "id": "20", + "relationships": { + "products": { + "data": [ + { "type": "products", "id": "1" } + ] + } + } + }); + t.deepEqual(store.find("products", "1").categories.map(x => x.id).sort(), [ "10", "20" ]); +}); + +test("add inverse relationships must setup inverse many-to-one relationships", function (t) { + var store = new Store(); + t.plan(1); + store.define("categories", { + products: Store.hasMany({ inverse: "category" }) + }); + store.define("products", { + category: Store.hasOne({ inverse: "products" }) + }); + store.add({ + "type": "categories", + "id": "10", + "relationships": { + "products": { + "data": [ + { "type": "products", "id": "1" } + ] + } + } + }); + store.add({ + "type": "categories", + "id": "20", + "relationships": { + "products": { + "data": [ + { "type": "products", "id": "1" } + ] + } + } + }); + t.equal(store.find("products", "1").category, store.find("categories", "20")); +}); + +test("add inverse relationships must throw an error when an inverse relationship is an attribute", function (t) { + var store = new Store(); + t.plan(2); + store.define("categories", { + product: Store.attr() + }); + store.define("comments", { + product: Store.attr() + }); + store.define("products", { + category: Store.hasOne({ inverse: "product" }), + comments: Store.hasMany({ inverse: "product" }) + }); + t.throws(function () { store.add({ "type": "products", "id": "44", - "attributes": { - "title": true + "relationships": { + "category": { + "data": { "type": "categories", "id": "34" } + } } }); - expect(store.find("products", "44").title).toBe("Example"); + }, /The the inverse relationship for 'category' is an attribute \('product'\)/); + t.throws(function () { store.add({ "type": "products", - "id": "44" + "id": "44", + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "3" } + ] + } + } }); - expect(store.find("products", "44").title).toBe("Example"); - }); + }, /The the inverse relationship for 'comments' is an attribute \('product'\)/); +}); - it("must set fields when the deserialize function returns null", function () { - store.define("products", { - title: { - deserialize: function (data) { - if (data.attributes && data.attributes.title) { - return "Example"; - } else { - return null; - } +test("add inverse relationships must throw an error when an explict inverse relationship is absent", function (t) { + var store = new Store(); + t.plan(3); + store.define("categories", {}); + store.define("comments", {}); + store.define("products", { + category: Store.hasOne({ inverse: "product" }), + comments: Store.hasMany({ inverse: "products" }), + users: Store.hasMany() + }); + store.define("users", {}); + t.throws(function () { + store.add({ + "type": "products", + "id": "44", + "relationships": { + "category": { + "data": { "type": "categories", "id": "34" } } } }); + }, /The the inverse relationship for 'category' is missing \('product'\)/); + t.throws(function () { store.add({ "type": "products", "id": "44", - "attributes": { - "title": true + "relationships": { + "comments": { + "data": [ + { "type": "comments", "id": "3" } + ] + } } }); - expect(store.find("products", "44").title).toBe("Example"); + }, /The the inverse relationship for 'comments' is missing \('products'\)/); + t.doesNotThrow(function () { store.add({ "type": "products", - "id": "44" - }); - expect(store.find("products", "44").title).toBe(null); - }); - - it("must call deserialize functions in the context of the store", function () { - store.define("products", { - example: { - deserialize: function () { - return this; + "id": "44", + "relationships": { + "users": { + "data": [ + { "type": "users", "id": "6" } + ] } } }); + }); +}); + +test("add inverse relationships must not try to process inverse relationships for absent relationships", function (t) { + var store = new Store(); + t.plan(1); + store.define("categories", { + products: Store.hasMany({ inverse: "category" }) + }); + store.define("products", { + category: Store.hasOne({ inverse: "products" }) + }); + t.doesNotThrow(function () { store.add({ "type": "products", "id": "44" }); - expect(store.find("products", "44").example).toBe(store); }); +}); - describe("inverse relationships", function () { - - it("must setup inverse one-to-one relationships", function () { - store.define("categories", { - product: Store.hasOne({ inverse: "category" }) - }); - store.define("products", { - category: Store.hasOne({ inverse: "product" }) - }); - store.add({ - "type": "products", - "id": "10", - "relationships": { - "category": { - "data": { "type": "categories", "id": "1" } - } - } - }); - expect(store.find("categories", "1").product).toBe(store.find("products", "10")); - }); - - it("must setup inverse one-to-many relationships", function () { - store.define("categories", { - products: Store.hasMany({ inverse: "category" }) - }); - store.define("products", { - category: Store.hasOne({ inverse: "products" }) - }); - store.add({ - "type": "products", - "id": "10", - "relationships": { - "category": { - "data": { "type": "categories", "id": "1" } - } - } - }); - store.add({ - "type": "products", - "id": "20", - "relationships": { - "category": { - "data": { "type": "categories", "id": "1" } - } - } - }); - expect(store.find("categories", "1").products.map(x => x.id).sort()).toEqual([ "10", "20" ]); - }); - - it("must setup inverse many-to-many relationships", function () { - store.define("categories", { - products: Store.hasMany({ inverse: "categories" }) - }); - store.define("products", { - categories: Store.hasMany({ inverse: "products" }) - }); - store.add({ - "type": "categories", - "id": "10", - "relationships": { - "products": { - "data": [ - { "type": "products", "id": "1" } - ] - } - } - }); - store.add({ - "type": "categories", - "id": "20", - "relationships": { - "products": { - "data": [ - { "type": "products", "id": "1" } - ] - } - } - }); - expect(store.find("products", "1").categories.map(x => x.id).sort()).toEqual([ "10", "20" ]); - }); - - it("must setup inverse many-to-one relationships", function () { - store.define("categories", { - products: Store.hasMany({ inverse: "category" }) - }); - store.define("products", { - category: Store.hasOne({ inverse: "products" }) - }); - store.add({ - "type": "categories", - "id": "10", - "relationships": { - "products": { - "data": [ - { "type": "products", "id": "1" } - ] - } - } - }); - store.add({ - "type": "categories", - "id": "20", - "relationships": { - "products": { - "data": [ - { "type": "products", "id": "1" } - ] - } - } - }); - expect(store.find("products", "1").category).toBe(store.find("categories", "20")); - }); - - it("must throw an error when an inverse relationship is an attribute", function () { - store.define("categories", { - product: Store.attr() - }); - store.define("comments", { - product: Store.attr() - }); - store.define("products", { - category: Store.hasOne({ inverse: "product" }), - comments: Store.hasMany({ inverse: "product" }), - }); - expect(function () { - store.add({ - "type": "products", - "id": "44", - "relationships": { - "category": { - "data": { "type": "categories", "id": "34" } - } - } - }); - }).toThrowError("The the inverse relationship for 'category' is an attribute ('product')"); - expect(function () { - store.add({ - "type": "products", - "id": "44", - "relationships": { - "comments": { - "data": [ - { "type": "comments", "id": "3" } - ] - } - } - }); - }).toThrowError("The the inverse relationship for 'comments' is an attribute ('product')"); - }); - - it("must throw an error when an explict inverse relationship is absent", function () { - store.define("categories", {}); - store.define("comments", {}); - store.define("products", { - category: Store.hasOne({ inverse: "product" }), - comments: Store.hasMany({ inverse: "products" }), - users: Store.hasMany() - }); - store.define("users", {}); - expect(function () { - store.add({ - "type": "products", - "id": "44", - "relationships": { - "category": { - "data": { "type": "categories", "id": "34" } - } - } - }); - }).toThrowError("The the inverse relationship for 'category' is missing ('product')"); - expect(function () { - store.add({ - "type": "products", - "id": "44", - "relationships": { - "comments": { - "data": [ - { "type": "comments", "id": "3" } - ] - } - } - }); - }).toThrowError("The the inverse relationship for 'comments' is missing ('products')"); - expect(function () { - store.add({ - "type": "products", - "id": "44", - "relationships": { - "users": { - "data": [ - { "type": "users", "id": "6" } - ] - } - } - }); - }).not.toThrow(); - }); - - it("must not try to process inverse relationships for absent relationships", function () { - store.define("categories", { - products: Store.hasMany({ inverse: "category" }) - }); - store.define("products", { - category: Store.hasOne({ inverse: "products" }) - }); - expect(function () { - store.add({ - "type": "products", - "id": "44" - }); - }).not.toThrow(); - }); - - it("must remove null (has one) inverse relationships", function () { - store.define("categories", { - products: Store.hasMany({ inverse: "category" }) - }); - store.define("products", { - category: Store.hasOne({ inverse: "products" }) - }); - store.add({ - "type": "products", - "id": "44", - "relationships": { - "category": { - "data": { "type": "categories", "id": "34" } - } - } - }); - expect(store.find("categories", "34").products.map(x => x.id).sort()).toEqual([ "44" ]); - store.add({ - "type": "products", - "id": "44", - "relationships": { - "category": { - "data": null - } - } - }); - expect(store.find("categories", "34").products).toEqual([]); - }); - - it("must remove empty (has many) inverse relationships", function () { - store.define("categories", { - products: Store.hasMany({ inverse: "category" }) - }); - store.define("products", { - category: Store.hasOne({ inverse: "products" }) - }); - store.add({ - "type": "categories", - "id": "37", - "relationships": { - "products": { - "data": [ - { "type": "products", "id": "23" }, - { "type": "products", "id": "45" } - ] - } - } - }); - expect(store.find("products", "23").category).toBe(store.find("categories", "37")); - expect(store.find("products", "45").category).toBe(store.find("categories", "37")); - store.add({ - "type": "categories", - "id": "37", - "relationships": { - "products": { - "data": [ - { "type": "products", "id": "23" } - ] - } - } - }); - expect(store.find("products", "45").category).toBe(null); - }); - - it("must use the type's name as a fallback for relationship names when adding resources", function () { - store.define("categories", { - products: Store.hasMany() - }); - store.define("products", { - categories: Store.hasMany() - }); - store.add({ - "type": "categories", - "id": "37", - "relationships": { - "products": { - "data": [ - { "type": "products", "id": "23" } - ] - } - } - }); - expect(store.find("categories", "37").products.map(x => x.id).sort()).toEqual([ "23" ]); - expect(store.find("products", "23").categories.map(x => x.id).sort()).toEqual([ "37" ]); - }); +test("add inverse relationships must remove null (has one) inverse relationships", function (t) { + var store = new Store(); + t.plan(2); + store.define("categories", { + products: Store.hasMany({ inverse: "category" }) + }); + store.define("products", { + category: Store.hasOne({ inverse: "products" }) + }); + store.add({ + "type": "products", + "id": "44", + "relationships": { + "category": { + "data": { "type": "categories", "id": "34" } + } + } + }); + t.deepEqual(store.find("categories", "34").products.map(x => x.id).sort(), [ "44" ]); + store.add({ + "type": "products", + "id": "44", + "relationships": { + "category": { + "data": null + } + } + }); + t.deepEqual(store.find("categories", "34").products, []); +}); +test("add inverse relationships must remove empty (has many) inverse relationships", function (t) { + var store = new Store(); + t.plan(3); + store.define("categories", { + products: Store.hasMany({ inverse: "category" }) + }); + store.define("products", { + category: Store.hasOne({ inverse: "products" }) + }); + store.add({ + "type": "categories", + "id": "37", + "relationships": { + "products": { + "data": [ + { "type": "products", "id": "23" }, + { "type": "products", "id": "45" } + ] + } + } + }); + t.equal(store.find("products", "23").category, store.find("categories", "37")); + t.equal(store.find("products", "45").category, store.find("categories", "37")); + store.add({ + "type": "categories", + "id": "37", + "relationships": { + "products": { + "data": [ + { "type": "products", "id": "23" } + ] + } + } }); + t.equal(store.find("products", "45").category, null); +}); +test("add inverse relationships must use the type's name as a fallback for relationship names when adding resources", function (t) { + var store = new Store(); + t.plan(2); + store.define("categories", { + products: Store.hasMany() + }); + store.define("products", { + categories: Store.hasMany() + }); + store.add({ + "type": "categories", + "id": "37", + "relationships": { + "products": { + "data": [ + { "type": "products", "id": "23" } + ] + } + } + }); + t.deepEqual(store.find("categories", "37").products.map(x => x.id).sort(), [ "23" ]); + t.deepEqual(store.find("products", "23").categories.map(x => x.id).sort(), [ "37" ]); }); diff --git a/spec/store/core/define-spec.js b/spec/store/core/define-spec.js index 0532a76..c187998 100644 --- a/spec/store/core/define-spec.js +++ b/spec/store/core/define-spec.js @@ -1,43 +1,38 @@ -var Store = require("../../../src/store"); +import test from "tape"; +import Store from "../../../src/store"; -describe("define", function () { - - var store; - - beforeEach(function () { - store = new Store(); +test("define must accept pseudonyms", function (t) { + var store = new Store(); + t.plan(4); + store.define([ "comments", "comment" ], { + product: Store.hasOne() }); - - it("must accept pseudonyms", function () { - store.define([ "comments", "comment" ], { - product: Store.hasOne() - }); - store.define([ "products", "product" ], { - comments: Store.hasMany() - }); - expect(store.find("comment", "67")).toBe(store.find("comments", "67")); - expect(store.find("product", "8")).toBe(store.find("products", "8")); - store.add({ - "type": "comments", - "id": "1", - "relationships": { - "product": { - "data": { - "type": "products", - "id": "1" - } + store.define([ "products", "product" ], { + comments: Store.hasMany() + }); + t.equal(store.find("comment", "67"), store.find("comments", "67")); + t.equal(store.find("product", "8"), store.find("products", "8")); + store.add({ + "type": "comments", + "id": "1", + "relationships": { + "product": { + "data": { + "type": "products", + "id": "1" } } - }); - expect(store.find("product", "1").comments[0]).toBe(store.find("comment", "1")); - expect(store.find("comments", "1").product).toBe(store.find("products", "1")); - }); - - it("must throw an error if you try to define a type that has already been defined", function () { - store.define("example", {}); - expect(function () { - store.define([ "sample", "example"], {}); - }).toThrowError("The type 'example' has already been defined."); + } }); + t.equal(store.find("product", "1").comments[0], store.find("comment", "1")); + t.equal(store.find("comments", "1").product, store.find("products", "1")); +}); +test("define must throw an error if you try to define a type that has already been defined", function (t) { + var store = new Store(); + t.plan(1); + store.define("example", {}); + t.throws(function () { + store.define([ "sample", "example"], {}); + }, /The type 'example' has already been defined\./); }); diff --git a/spec/store/core/find-spec.js b/spec/store/core/find-spec.js index 256c5bf..62b621b 100644 --- a/spec/store/core/find-spec.js +++ b/spec/store/core/find-spec.js @@ -1,52 +1,55 @@ -var Store = require("../../../src/store"); - -describe("find", function() { - - var store; - - beforeEach(function() { - store = new Store(); - }); +import test from "tape"; +import Store from "../../../src/store"; + +test("find must, when an id is provided, return an object with 'type' and 'id' properties", function (t) { + var store = new Store(); + t.plan(2); + store.define("products", {}); + t.equal(store.find("products", "23").type, "products"); + t.equal(store.find("products", "74").id, "74"); +}); - it("must, when an id is provided, return an object with 'type' and 'id' properties", function () { - store.define("products", {}); - expect(store.find("products", "23").type).toBe("products"); - expect(store.find("products", "74").id).toBe("74"); - }); +test("find must return the same object if called with the same arguments", function (t) { + var store = new Store(); + t.plan(1); + store.define("products", {}); + t.equal(store.find("products", "23"), store.find("products", "23")); +}); - it("must return the same object if called with the same arguments", function () { - store.define("products", {}); - expect(store.find("products", "23")).toBe(store.find("products", "23")); - }); +test("find must return an array of objects when no id is provided", function (t) { + var store = new Store(); + t.plan(1); + store.define("products", {}); + store.find("products", "1"); + t.notEqual(store.find("products").indexOf(store.find("products", "1")), -1); +}); - it("must return an array of objects when no id is provided", function () { - store.define("products", {}); +test("find must throw an error when trying to find an unknown resource type", function (t) { + var store = new Store(); + t.plan(2); + t.throws(function () { + store.find("products"); + }, /Unknown type 'products'/); + t.throws(function () { store.find("products", "1"); - expect(store.find("products")).toContain(store.find("products", "1")); - }); - - it("must throw an error when trying to find an unknown resource type", function () { - expect(function () { - store.find("products"); - }).toThrowError(TypeError, "Unknown type 'products'"); - expect(function () { - store.find("products", "1"); - }).toThrowError(TypeError, "Unknown type 'products'"); - }); + }, /Unknown type 'products'/); +}); - it("must throw an error when called without arguments", function () { - expect(function () { - store.find(); - }).toThrowError(TypeError, "You must provide a type"); - }); +test("find must throw an error when called without arguments", function (t) { + var store = new Store(); + t.plan(1); + t.throws(function () { + store.find(); + }, /You must provide a type/); +}); - it("must give fields their default values", function () { - store.define("products", { - title: { - default: "example" - } - }); - expect(store.find("products", "1").title).toBe("example"); +test("find must give fields their default values", function (t) { + var store = new Store(); + t.plan(1); + store.define("products", { + title: { + default: "example" + } }); - + t.equal(store.find("products", "1").title, "example"); }); diff --git a/spec/store/core/push-spec.js b/spec/store/core/push-spec.js index cedbea4..df8499d 100644 --- a/spec/store/core/push-spec.js +++ b/spec/store/core/push-spec.js @@ -1,69 +1,67 @@ -var Store = require("../../../src/store"); +import test from "tape"; +import sinon from "sinon"; +import Store from "../../../src/store"; -describe("push", function() { - - var store; - - beforeEach(function() { - store = new Store(); - }); +test("push must add a single resource to the store", function (t) { + var store = new Store(); + t.plan(1); + store.define("products", {}); + sinon.spy(store, "add"); + var root = { + "data": { + "type": "products", + "id": "34" + } + }; + store.push(root); + t.ok(store.add.calledWith(root.data), "should call add with data pushed"); +}); - it("must add a single resource to the store", function () { - store.define("products", {}); - spyOn(store, 'add'); - var root = { - "data": { +test("push must add a collection of resources to the store", function (t) { + var store = new Store(); + t.plan(2); + store.define("products", {}); + sinon.spy(store, "add"); + var root = { + "data": [ + { "type": "products", "id": "34" + }, + { + "type": "products", + "id": "74" } - }; - store.push(root); - expect(store.add).toHaveBeenCalledWith(root.data); - }); - - it("must add a collection of resources to the store", function () { - store.define("products", {}); - spyOn(store, 'add'); - var root = { - "data": [ - { - "type": "products", - "id": "34" - }, - { - "type": "products", - "id": "74" - } - ] - }; - store.push(root); - expect(store.add).toHaveBeenCalledWith(root.data[0]); - expect(store.add).toHaveBeenCalledWith(root.data[1]); - }); + ] + }; + store.push(root); + t.ok(store.add.calledWith(root.data[0]), "should call add with data pushed"); + t.ok(store.add.calledWith(root.data[1]), "should call add with data pushed"); +}); - it("must add included resources to the store", function () { - store.define("categories", {}); - store.define("products", {}); - spyOn(store, 'add'); - var root = { - "data": { - "type": "categories", +test("push must add included resources to the store", function (t) { + var store = new Store(); + t.plan(2); + store.define("categories", {}); + store.define("products", {}); + sinon.spy(store, "add"); + var root = { + "data": { + "type": "categories", + "id": "34" + }, + "included": [ + { + "type": "products", "id": "34" }, - "included": [ - { - "type": "products", - "id": "34" - }, - { - "type": "products", - "id": "74" - } - ] - }; - store.push(root); - expect(store.add).toHaveBeenCalledWith(root.included[0]); - expect(store.add).toHaveBeenCalledWith(root.included[1]); - }); - + { + "type": "products", + "id": "74" + } + ] + }; + store.push(root); + t.ok(store.add.calledWith(root.included[0]), "should call add with data pushed"); + t.ok(store.add.calledWith(root.included[1]), "should call add with data pushed"); }); diff --git a/spec/store/core/remove-spec.js b/spec/store/core/remove-spec.js index ff3a85b..e83d26e 100644 --- a/spec/store/core/remove-spec.js +++ b/spec/store/core/remove-spec.js @@ -1,78 +1,80 @@ -var Store = require("../../../src/store"); +import test from "tape"; +import sinon from "sinon"; +import Store from "../../../src/store"; -describe("remove", function() { - - var store; - - beforeEach(function() { - store = new Store(); +test("remove must remove a resource from the store", function (t) { + var store = new Store(); + t.plan(2); + store.define("products", {}); + store.add({ + "type": "products", + "id": "44" }); + t.equal(store.find("products").length, 1); + store.remove("products", "44"); + t.equal(store.find("products").length, 0); +}); - it("must remove a resource from the store", function () { - store.define("products", {}); - store.add({ - "type": "products", - "id": "44" - }); - expect(store.find("products").length).toBe(1); - store.remove("products", "44"); - expect(store.find("products").length).toBe(0); +test("remove must remove call remove for each resources of a type if no id is given", function (t) { + var store = new Store(); + t.plan(4); + store.define("products", {}); + store.add({ + "type": "products", + "id": "44" }); + store.add({ + "type": "products", + "id": "47" + }); + t.equal(store.find("products").length, 2); + sinon.spy(store, "remove"); + store.remove("products"); + t.equal(store.find("products").length, 0); + t.ok(store.remove.calledWith("products", "44")); + t.ok(store.remove.calledWith("products", "47")); +}); + +test("remove must throw an error when called without arguments", function (t) { + var store = new Store(); + t.plan(1); + t.throws(function () { + store.remove(); + }, /You must provide a type to remove/); +}); - it("must remove call remove for each resources of a type if no id is given", function () { - store.define("products", {}); - store.add({ - "type": "products", - "id": "44" - }); - store.add({ - "type": "products", - "id": "47" - }); - expect(store.find("products").length).toBe(2); - spyOn(store, 'remove').and.callThrough(); +test("remove must throw an error if the type has not been defined", function (t) { + var store = new Store(); + t.plan(1); + t.throws(function () { store.remove("products"); - expect(store.find("products").length).toBe(0); - expect(store.remove).toHaveBeenCalledWith("products", "44"); - expect(store.remove).toHaveBeenCalledWith("products", "47"); - }); + }, /Unknown type 'products'/); +}); - it("must throw an error when called without arguments", function () { - expect(function () { - store.remove(); - }).toThrowError(TypeError, "You must provide a type to remove"); +test("remove must remove dependant relationships when a resource is removed", function (t) { + var store = new Store(); + t.plan(3); + store.define("categories", { + products: Store.hasMany({ inverse: "category" }) }); - - it("must throw an error if the type has not been defined", function () { - expect(function () { - store.remove("products"); - }).toThrowError(TypeError, "Unknown type 'products'"); + store.define("products", { + category: Store.hasOne({ inverse: "products" }) }); - - it("must remove dependant relationships when a resource is removed", function () { - store.define("categories", { - products: Store.hasMany({ inverse: "category" }) - }); - store.define("products", { - category: Store.hasOne({ inverse: "products" }) - }); - store.add({ - "type": "categories", - "id": "1", - "relationships": { - "products": { - "data": [ - { "type": "products", "id": "10" }, - { "type": "products", "id": "11" } - ] - } + store.add({ + "type": "categories", + "id": "1", + "relationships": { + "products": { + "data": [ + { "type": "products", "id": "10" }, + { "type": "products", "id": "11" } + ] } - }); - expect(store.find("categories", "1").products.map(x => x.id).sort()).toEqual([ "10", "11" ]); - store.remove("products", "10"); - expect(store.find("categories", "1").products.map(x => x.id).sort()).toEqual([ "11" ]); - store.remove("categories", "1"); - expect(store.find("products", "11").category).toBe(null); + } }); - + t.deepEqual(store.find("categories", "1").products.map(x => x.id).sort(), [ "10", "11" ]); + store.remove("products", "10"); + t.deepEqual(store.find("categories", "1").products.map(x => x.id).sort(), [ "11" ]); + store.remove("categories", "1"); + t.equal(store.find("products", "11").category, null); }); diff --git a/spec/store/events/off-spec.js b/spec/store/events/off-spec.js index a48bf7d..3e03dc4 100644 --- a/spec/store/events/off-spec.js +++ b/spec/store/events/off-spec.js @@ -1,74 +1,77 @@ -var Store = require("../../../src/store"); +import test from "tape"; +import sinon from "sinon"; +import Store from "../../../src/store"; -describe("off", function() { - - var store, listener, context; - - beforeEach(function() { - store = new Store(); - listener = { - handler: function () {} - }; - context = {}; - spyOn(listener, "handler"); - store.define([ "products", "product" ], {}); +test("off must remove event handlers", function (t) { + var store = new Store(); + var listener = sinon.spy(); + var context = {}; + t.plan(2); + store.define([ "products", "product" ], {}); + store.on("added", "products", "1", listener, context); + store.on("added", "products", listener, context); + store.on("updated", "products", "1", listener, context); + store.on("updated", "products", listener, context); + store.on("removed", "products", "1", listener, context); + store.on("removed", "products", listener, context); + store.add({ + "type": "products", + "id": "1" }); - - it("must remove event handlers", function () { - store.on("added", "products", "1", listener.handler, context); - store.on("added", "products", listener.handler, context); - store.on("updated", "products", "1", listener.handler, context); - store.on("updated", "products", listener.handler, context); - store.on("removed", "products", "1", listener.handler, context); - store.on("removed", "products", listener.handler, context); - store.add({ - "type": "products", - "id": "1" - }); - store.add({ - "type": "products", - "id": "1" - }); - store.remove("products", "1"); - expect(listener.handler.calls.count()).toEqual(6); - store.off("added", "products", "1", listener.handler); - store.off("added", "products", listener.handler); - store.off("updated", "products", "1", listener.handler); - store.off("updated", "products", listener.handler); - store.off("removed", "products", "1", listener.handler); - store.off("removed", "products", listener.handler); - store.add({ - "type": "products", - "id": "1" - }); - store.add({ - "type": "products", - "id": "1" - }); - store.remove("products", "1"); - expect(listener.handler.calls.count()).toEqual(6); + store.add({ + "type": "products", + "id": "1" }); - - it("must throw an error when an unknown event is passed", function () { - expect(function () { - store.off("foo", "products", "1", listener.handler); - }).toThrowError("Unknown event 'foo'"); + store.remove("products", "1"); + t.equal(listener.callCount, 6); + store.off("added", "products", "1", listener); + store.off("added", "products", listener); + store.off("updated", "products", "1", listener); + store.off("updated", "products", listener); + store.off("removed", "products", "1", listener); + store.off("removed", "products", listener); + store.add({ + "type": "products", + "id": "1" }); - - it("must throw an error if the type has not been defined", function () { - expect(function () { - store.off("added", "foo", "1", listener.handler); - }).toThrowError("Unknown type 'foo'"); + store.add({ + "type": "products", + "id": "1" }); + store.remove("products", "1"); + t.equal(listener.callCount, 6); +}); - it("must remove listeners added with a different pseudonym", function () { - store.on("added", "products", listener.handler, context); - store.off("added", "product", listener.handler); - store.add({ - "type": "products", - "id": "1" - }); - expect(listener.handler.calls.count()).toEqual(0); - }); +test("off must throw an error when an unknown event is passed", function (t) { + var store = new Store(); + var listener = sinon.spy(); + t.plan(1); + store.define([ "products", "product" ], {}); + t.throws(function () { + store.off("foo", "products", "1", listener); + }, /Unknown event 'foo'/); +}); +test("off must throw an error if the type has not been defined", function (t) { + var store = new Store(); + var listener = sinon.spy(); + t.plan(1); + store.define([ "products", "product" ], {}); + t.throws(function () { + store.off("added", "foo", "1", listener); + }, /Unknown type 'foo'/); +}); + +test("off must remove listeners added with a different pseudonym", function (t) { + var store = new Store(); + var listener = sinon.spy(); + t.plan(1); + store.define([ "products", "product" ], {}); + store.on("added", "products", listener); + store.off("added", "product", listener); + store.add({ + "type": "products", + "id": "1" + }); + t.equal(listener.callCount, 0); }); diff --git a/spec/store/events/on-spec.js b/spec/store/events/on-spec.js index 68d1b5e..80ca626 100644 --- a/spec/store/events/on-spec.js +++ b/spec/store/events/on-spec.js @@ -1,168 +1,224 @@ -var Store = require("../../../src/store"); - -describe("on", function() { - - var store, listener, context; - - beforeEach(function() { - store = new Store(); - listener = { - handler: function () {} - }; - context = {}; - spyOn(listener, "handler"); - store.define([ "products", "product" ], {}); +import test from "tape"; +import sinon from "sinon"; +import Store from "../../../src/store"; + +test("on must fire an added event when the resource with the given type & id is added to the store", function (t) { + var store = new Store(); + var listener = sinon.spy(); + var context = {}; + t.plan(2); + store.define([ "products", "product" ], {}); + store.on("added", "products", "1", listener, context); + store.add({ + "type": "products", + "id": "1" }); + t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); + t.ok(listener.calledOn(context), "should be called on context given"); +}); - it("must fire an added event when the resource with the given type & id is added to the store", function () { - store.on("added", "products", "1", listener.handler, context); - store.add({ - "type": "products", - "id": "1" - }); - expect(listener.handler).toHaveBeenCalledWith(store.find("products", "1")); - expect(listener.handler.calls.mostRecent().object).toBe(context); +test("on must fire an added event when the resource with the given type is added to the store", function (t) { + var store = new Store(); + var listener = sinon.spy(); + var context = {}; + t.plan(2); + store.define([ "products", "product" ], {}); + store.on("added", "products", listener, context); + store.add({ + "type": "products", + "id": "1" }); + t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); + t.ok(listener.calledOn(context), "should be called on context given"); +}); - it("must fire an added event when the resource with the given type is added to the store", function () { - store.on("added", "products", listener.handler, context); - store.add({ - "type": "products", - "id": "1" - }); - expect(listener.handler).toHaveBeenCalledWith(store.find("products", "1")); - expect(listener.handler.calls.mostRecent().object).toBe(context); +test("on must fire an updated event when the resource with the given type & id is updated in the store", function (t) { + var store = new Store(); + var listener = sinon.spy(); + var context = {}; + t.plan(3); + store.define([ "products", "product" ], {}); + store.on("updated", "products", "1", listener, context); + store.add({ + "type": "products", + "id": "1" }); - - it("must fire an updated event when the resource with the given type & id is updated in the store", function () { - store.on("updated", "products", "1", listener.handler, context); - store.add({ - "type": "products", - "id": "1" - }); - expect(listener.handler.calls.count()).toEqual(0); - store.add({ - "type": "products", - "id": "1" - }); - expect(listener.handler).toHaveBeenCalledWith(store.find("products", "1")); - expect(listener.handler.calls.mostRecent().object).toBe(context); + t.equal(listener.callCount, 0); + store.add({ + "type": "products", + "id": "1" }); + t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); + t.ok(listener.calledOn(context), "should be called on context given"); +}); - it("must fire an updated event when the resource with the given type is updated in the store", function () { - store.on("updated", "products", listener.handler, context); - store.add({ - "type": "products", - "id": "1" - }); - expect(listener.handler.calls.count()).toEqual(0); - store.add({ - "type": "products", - "id": "1" - }); - expect(listener.handler).toHaveBeenCalledWith(store.find("products", "1")); - expect(listener.handler.calls.mostRecent().object).toBe(context); +test("on must fire an updated event when the resource with the given type is updated in the store", function (t) { + var store = new Store(); + var listener = sinon.spy(); + var context = {}; + t.plan(3); + store.define([ "products", "product" ], {}); + store.on("updated", "products", listener, context); + store.add({ + "type": "products", + "id": "1" }); - - it("must fire an removed event when the resource with the given type & id is removed from the store", function () { - store.on("removed", "products", "1", listener.handler, context); - store.add({ - "type": "products", - "id": "1" - }); - expect(listener.handler.calls.count()).toEqual(0); - store.remove("products", "1"); - expect(listener.handler).toHaveBeenCalledWith(store.find("products", "1")); - expect(listener.handler.calls.mostRecent().object).toBe(context); + t.equal(listener.callCount, 0); + store.add({ + "type": "products", + "id": "1" }); + t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); + t.ok(listener.calledOn(context), "should be called on context given"); +}); - it("must fire an removed event when the resource with the given type is removed from the store", function () { - store.on("removed", "products", listener.handler, context); - store.add({ - "type": "products", - "id": "1" - }); - expect(listener.handler.calls.count()).toEqual(0); - store.remove("products", "1"); - expect(listener.handler).toHaveBeenCalledWith(store.find("products", "1")); - expect(listener.handler.calls.mostRecent().object).toBe(context); +test("on must fire an removed event when the resource with the given type & id is removed from the store", function (t) { + var store = new Store(); + var listener = sinon.spy(); + var context = {}; + t.plan(3); + store.define([ "products", "product" ], {}); + store.on("removed", "products", "1", listener, context); + store.add({ + "type": "products", + "id": "1" }); + t.equal(listener.callCount, 0); + store.remove("products", "1"); + t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); + t.ok(listener.calledOn(context), "should be called on context given"); +}); - it("must not fire an added event when automatically creating a resource as the result of a find", function () { - store.on("added", "products", "1", listener.handler, context); - store.find("products", "1"); - expect(listener.handler.calls.count()).toEqual(0); +test("on must fire an removed event when the resource with the given type is removed from the store", function (t) { + var store = new Store(); + var listener = sinon.spy(); + var context = {}; + t.plan(3); + store.define([ "products", "product" ], {}); + store.on("removed", "products", listener, context); + store.add({ + "type": "products", + "id": "1" }); + t.equal(listener.callCount, 0); + store.remove("products", "1"); + t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); + t.ok(listener.calledOn(context), "should be called on context given"); +}); - it("must throw an error when an unknown event is passed", function () { - expect(function () { - store.on("foo", "products", "1", listener.handler, context); - }).toThrowError("Unknown event 'foo'"); - }); +test("on must not fire an added event when automatically creating a resource as the result of a find", function (t) { + var store = new Store(); + var listener = sinon.spy(); + var context = {}; + t.plan(1); + store.define([ "products", "product" ], {}); + store.on("added", "products", "1", listener, context); + store.find("products", "1"); + t.equal(listener.callCount, 0); +}); - it("must throw an error if the type has not been defined", function () { - expect(function () { - store.on("added", "foo", "1", listener.handler, context); - }).toThrowError("Unknown type 'foo'"); - }); +test("on must throw an error when an unknown event is passed", function (t) { + var store = new Store(); + var listener = sinon.spy(); + var context = {}; + t.plan(1); + store.define([ "products", "product" ], {}); + t.throws(function () { + store.on("foo", "products", "1", listener, context); + }, /Unknown event 'foo'/); +}); - it("must call listeners that were added using a different pseudonym", function () { - store.on("added", "product", listener.handler, context); - store.add({ - "type": "products", - "id": "1" - }); - expect(listener.handler.calls.count()).toEqual(1); - expect(listener.handler).toHaveBeenCalledWith(store.find("products", "1")); - }); +test("on must throw an error if the type has not been defined", function (t) { + var store = new Store(); + var listener = sinon.spy(); + var context = {}; + t.plan(1); + store.define([ "products", "product" ], {}); + t.throws(function () { + store.on("added", "foo", "1", listener, context); + }, /Unknown type 'foo'/); +}); - it("must not add the same listener multiple times for the same type", function () { - store.on("added", "products", listener.handler, context); - store.on("added", "products", listener.handler); - store.add({ - "type": "products", - "id": "1" - }); - expect(listener.handler.calls.count()).toEqual(1); - expect(listener.handler).toHaveBeenCalledWith(store.find("products", "1")); +test("on must call listeners that were added using a different pseudonym", function (t) { + var store = new Store(); + var listener = sinon.spy(); + var context = {}; + t.plan(2); + store.define([ "products", "product" ], {}); + store.on("added", "product", listener, context); + store.add({ + "type": "products", + "id": "1" }); + t.equal(listener.callCount, 1); + t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); +}); - it("must not add the same listener multiple times for the same type and id", function () { - store.on("added", "products", "1", listener.handler, context); - store.on("added", "products", "1", listener.handler); - store.add({ - "type": "products", - "id": "1" - }); - expect(listener.handler.calls.count()).toEqual(1); - expect(listener.handler).toHaveBeenCalledWith(store.find("products", "1")); +test("on must not add the same listener multiple times for the same type", function (t) { + var store = new Store(); + var listener = sinon.spy(); + var context = {}; + t.plan(2); + store.define([ "products", "product" ], {}); + store.on("added", "products", listener, context); + store.on("added", "products", listener); + store.add({ + "type": "products", + "id": "1" }); + t.equal(listener.callCount, 1); + t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); +}); - it("must add the same listener multiple times if the id is different", function () { - store.on("added", "products", "1", listener.handler, context); - store.on("added", "products", "2", listener.handler, context); - store.add({ - "type": "products", - "id": "1" - }); - store.add({ - "type": "products", - "id": "2" - }); - expect(listener.handler.calls.count()).toEqual(2); - expect(listener.handler).toHaveBeenCalledWith(store.find("products", "1")); - expect(listener.handler).toHaveBeenCalledWith(store.find("products", "2")); +test("on must not add the same listener multiple times for the same type and id", function (t) { + var store = new Store(); + var listener = sinon.spy(); + var context = {}; + t.plan(2); + store.define([ "products", "product" ], {}); + store.on("added", "products", "1", listener, context); + store.on("added", "products", "1", listener); + store.add({ + "type": "products", + "id": "1" }); + t.equal(listener.callCount, 1); + t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); +}); - it("must not add the same listener multiple times for different pseudonyms of the same type", function () { - store.on("added", "products", listener.handler, context); - store.on("added", "product", listener.handler, context); - store.add({ - "type": "products", - "id": "1" - }); - expect(listener.handler.calls.count()).toEqual(1); - expect(listener.handler).toHaveBeenCalledWith(store.find("products", "1")); +test("on must add the same listener multiple times if the id is different", function (t) { + var store = new Store(); + var listener = sinon.spy(); + var context = {}; + t.plan(3); + store.define([ "products", "product" ], {}); + store.on("added", "products", "1", listener, context); + store.on("added", "products", "2", listener, context); + store.add({ + "type": "products", + "id": "1" + }); + store.add({ + "type": "products", + "id": "2" }); + t.equal(listener.callCount, 2); + t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); + t.ok(listener.calledWith(store.find("products", "2")), "called with resource"); +}); +test("on must not add the same listener multiple times for different pseudonyms of the same type", function (t) { + var store = new Store(); + var listener = sinon.spy(); + var context = {}; + t.plan(2); + store.define([ "products", "product" ], {}); + store.on("added", "products", listener, context); + store.on("added", "product", listener, context); + store.add({ + "type": "products", + "id": "1" + }); + t.equal(listener.callCount, 1); + t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); }); diff --git a/spec/store/fields/attr-spec.js b/spec/store/fields/attr-spec.js index 1603b9f..d94c4cd 100644 --- a/spec/store/fields/attr-spec.js +++ b/spec/store/fields/attr-spec.js @@ -1,57 +1,55 @@ -var Store = require("../../../src/store"); +import test from "tape"; +import Store from "../../../src/store"; -describe("attr", function() { - - var store; - - beforeEach(function() { - store = new Store(); - }); - - it("must return the correct type attribute", function () { - expect(Store.attr().type).toBe("attr"); - }); - - it("must return a deserialize function that passes on a default option", function () { - expect(Store.attr({ default: "foo" }).default).toBe("foo"); - expect(Store.attr("example", { default: "foo" }).default).toBe("foo"); - }); +test("attr must return the correct type attribute", function (t) { + t.plan(1); + t.equal(Store.attr().type, "attr"); +}); - it("must return a deserialize function that maps to the attribute provided", function () { - var field = Store.attr("example-title"); - var data = { - "type": "products", - "id": "1", - "attributes": { - "example-title": "Example" - } - }; - expect(field.deserialize.call(this, data)).toBe("Example"); - }); +test("attr must return a deserialize function that passes on a default option", function (t) { + t.plan(2); + t.equal(Store.attr({ default: "foo" }).default, "foo"); + t.equal(Store.attr("example", { default: "foo" }).default, "foo"); +}); - it("must return a deserialize function that maps to the key if no attribute name is provided", function () { - var field = Store.attr(); - var data = { - "type": "products", - "id": "1", - "attributes": { - "title": "Example" - } - }; - expect(field.deserialize.call(store, data, "title")).toBe("Example"); - }); +test("attr must return a deserialize function that maps to the attribute provided", function (t) { + var field = Store.attr("example-title"); + var data = { + "type": "products", + "id": "1", + "attributes": { + "example-title": "Example" + } + }; + t.plan(1); + t.equal(field.deserialize.call(this, data), "Example"); +}); - it("must return undefined when the attribute is missing from the data", function () { - var field = Store.attr("title"); - expect(field.deserialize.call(store, { - "type": "products", - "id": "1", - "attributes": {} - }, "title")).toBeUndefined(); - expect(field.deserialize.call(store, { - "type": "products", - "id": "1" - }, "title")).toBeUndefined(); - }); +test("attr must return a deserialize function that maps to the key if no attribute name is provided", function (t) { + var store = new Store(); + var field = Store.attr(); + var data = { + "type": "products", + "id": "1", + "attributes": { + "title": "Example" + } + }; + t.plan(1); + t.equal(field.deserialize.call(store, data, "title"), "Example"); +}); +test("attr must return undefined when the attribute is missing from the data", function (t) { + var store = new Store(); + var field = Store.attr("title"); + t.plan(2); + t.equal(field.deserialize.call(store, { + "type": "products", + "id": "1", + "attributes": {} + }, "title"), undefined); + t.equal(field.deserialize.call(store, { + "type": "products", + "id": "1" + }, "title"), undefined); }); diff --git a/spec/store/fields/has-many-spec.js b/spec/store/fields/has-many-spec.js index cb8a6dc..2fe9060 100644 --- a/spec/store/fields/has-many-spec.js +++ b/spec/store/fields/has-many-spec.js @@ -1,156 +1,156 @@ -var Store = require("../../../src/store"); +import test from "tape"; +import Store from "../../../src/store"; -describe("hasMany", function() { - - var store; - - beforeEach(function() { - store = new Store(); - }); - - it("must return the correct type attribute", function () { - expect(Store.hasMany().type).toBe("has-many"); - expect(Store.hasMany("example").type).toBe("has-many"); - expect(Store.hasMany({}).type).toBe("has-many"); - expect(Store.hasMany("example", {}).type).toBe("has-many"); - }); +test("hasMany must return the correct type attribute", function (t) { + t.plan(4); + t.equal(Store.hasMany().type, "has-many"); + t.equal(Store.hasMany("example").type, "has-many"); + t.equal(Store.hasMany({}).type, "has-many"); + t.equal(Store.hasMany("example", {}).type, "has-many"); +}); - it("must return default value as an empty array", function () { - expect(Store.hasMany().default).toEqual([]); - }); +test("hasMany must return default value as an empty array", function (t) { + t.plan(1); + t.deepEqual(Store.hasMany().default, []); +}); - it("must return a deserialize function that maps to the relation described in the data property", function () { - store.define("categories", {}); - store.define("products", {}); - var field = Store.hasMany("categories"); - var data = { - "type": "products", - "id": "1", - "relationships": { - "categories": { - "data": [ - { - "type": "categories", - "id": "2" - }, - { - "type": "categories", - "id": "4" - } - ] - } +test("hasMany must return a deserialize function that maps to the relation described in the data property", function (t) { + var store = new Store(); + var field = Store.hasMany("categories"); + var data = { + "type": "products", + "id": "1", + "relationships": { + "categories": { + "data": [ + { + "type": "categories", + "id": "2" + }, + { + "type": "categories", + "id": "4" + } + ] } - }; - expect(field.deserialize.call(store, data).length).toBe(2); - var ids = field.deserialize.call(store, data).map(function (category) { - return category.id; - }); - expect(ids).toContain("2"); - expect(ids).toContain("4"); - }); + } + }; + t.plan(2); + store.define("categories", {}); + store.define("products", {}); + t.equal(field.deserialize.call(store, data).length, 2); + t.deepEqual(field.deserialize.call(store, data).map(c => c.id).sort(), [ "2", "4" ]); +}); - it("must return a deserialize function that uses the key param when the name isn't provided", function () { - store.define("categories", {}); - store.define("products", {}); - var field = Store.hasMany(); - var data = { - "type": "products", - "id": "1", - "relationships": { - "categories": { - "data": [ - { - "type": "categories", - "id": "2" - }, - { - "type": "categories", - "id": "4" - } - ] - } +test("hasMany must return a deserialize function that uses the key param when the name isn't provided", function (t) { + var store = new Store(); + var field = Store.hasMany(); + var data = { + "type": "products", + "id": "1", + "relationships": { + "categories": { + "data": [ + { + "type": "categories", + "id": "2" + }, + { + "type": "categories", + "id": "4" + } + ] } - }; - expect(field.deserialize.call(store, data, "categories").length).toBe(2); - var ids = field.deserialize.call(store, data, "categories").map(function (category) { - return category.id; - }); - expect(ids).toContain("2"); - expect(ids).toContain("4"); - }); + } + }; + t.plan(2); + store.define("categories", {}); + store.define("products", {}); + t.equal(field.deserialize.call(store, data, "categories").length, 2); + t.deepEqual(field.deserialize.call(store, data, "categories").map(c => c.id).sort(), [ "2", "4" ]); +}); - it("must return a deserialize function that returns an empty array when the relationship data field is null or an empty array", function () { - var data = { - "type": "products", - "id": "1", - "relationships": { - "categories": { - "data": [] - }, - "comments": { - "data": null - } +test("hasMany must return a deserialize function that returns an empty array when the relationship data field is null or an empty array", function (t) { + var store = new Store(); + var data = { + "type": "products", + "id": "1", + "relationships": { + "categories": { + "data": [] + }, + "comments": { + "data": null } - }; - expect(Store.hasMany("categories").deserialize.call(store, data)).toEqual([]); - expect(Store.hasMany().deserialize.call(store, data, "categories")).toEqual([]); - expect(Store.hasMany("comments").deserialize.call(store, data)).toEqual([]); - expect(Store.hasMany().deserialize.call(store, data, "comments")).toEqual([]); - }); + } + }; + t.plan(4); + t.deepEqual(Store.hasMany("categories").deserialize.call(store, data), []); + t.deepEqual(Store.hasMany().deserialize.call(store, data, "categories"), []); + t.deepEqual(Store.hasMany("comments").deserialize.call(store, data), []); + t.deepEqual(Store.hasMany().deserialize.call(store, data, "comments"), []); +}); - it("must return a deserialize function that returns undefined when the relationship data field is missing", function () { - var data = { - "type": "products", - "id": "1", - "relationships": { - "categories": {} - } - }; - expect(Store.hasMany("categories").deserialize.call(store, data)).toBeUndefined(); - expect(Store.hasMany().deserialize.call(store, data, "categories")).toBeUndefined(); - }); +test("hasMany must return a deserialize function that returns undefined when the relationship data field is missing", function (t) { + var store = new Store(); + var data = { + "type": "products", + "id": "1", + "relationships": { + "categories": {} + } + }; + t.plan(2); + t.equal(Store.hasMany("categories").deserialize.call(store, data), undefined); + t.equal(Store.hasMany().deserialize.call(store, data, "categories"), undefined); +}); - it("must return a deserialize function that returns undefined when the relationship type field is missing", function () { - var data = { - "type": "products", - "id": "1", - "relationships": {} - }; - expect(Store.hasMany("categories").deserialize.call(store, data)).toBeUndefined(); - expect(Store.hasMany().deserialize.call(store, data, "categories")).toBeUndefined(); - }); +test("hasMany must return a deserialize function that returns undefined when the relationship type field is missing", function (t) { + var store = new Store(); + var data = { + "type": "products", + "id": "1", + "relationships": {} + }; + t.plan(2); + t.equal(Store.hasMany("categories").deserialize.call(store, data), undefined); + t.equal(Store.hasMany().deserialize.call(store, data, "categories"), undefined); +}); - it("must return a deserialize function that returns undefined when the relationship field is missing", function () { - var data = { - "type": "products", - "id": "1" - }; - expect(Store.hasMany("categories").deserialize.call(store, data)).toBeUndefined(); - expect(Store.hasMany().deserialize.call(store, data, "categories")).toBeUndefined(); - }); +test("hasMany must return a deserialize function that returns undefined when the relationship field is missing", function (t) { + var store = new Store(); + var data = { + "type": "products", + "id": "1" + }; + t.plan(2); + t.equal(Store.hasMany("categories").deserialize.call(store, data), undefined); + t.equal(Store.hasMany().deserialize.call(store, data, "categories"), undefined); +}); - it("must return a deserialize function that passes on an inverse option", function () { - expect(Store.hasMany({ inverse: "foo" }).inverse).toBe("foo"); - expect(Store.hasMany("example", { inverse: "foo" }).inverse).toBe("foo"); - }); +test("hasMany must return a deserialize function that passes on an inverse option", function (t) { + t.plan(2); + t.equal(Store.hasMany({ inverse: "foo" }).inverse, "foo"); + t.equal(Store.hasMany("example", { inverse: "foo" }).inverse, "foo"); +}); - it("must throw an error a relationship's type hasn't been defined", function () { - store.define("products", {}); - var field = Store.hasMany(); - var data = { - "type": "products", - "id": "44", - "relationships": { - "categories": { - "data": [ - { "type": "categories", "id": "34" } - ] - } +test("hasMany must throw an error a relationship's type hasn't been defined", function (t) { + var store = new Store(); + var field = Store.hasMany(); + var data = { + "type": "products", + "id": "44", + "relationships": { + "categories": { + "data": [ + { "type": "categories", "id": "34" } + ] } - }; - expect(function () { - field.deserialize.call(store, data, "categories"); - }).toThrowError("Unknown type 'categories'"); - }); - + } + }; + t.plan(1); + store.define("products", {}); + t.throws(function () { + field.deserialize.call(store, data, "categories"); + }, /Unknown type 'categories'/); }); diff --git a/spec/store/fields/has-one-spec.js b/spec/store/fields/has-one-spec.js index cbe1fd6..0b95dd3 100644 --- a/spec/store/fields/has-one-spec.js +++ b/spec/store/fields/has-one-spec.js @@ -1,123 +1,130 @@ -var Store = require("../../../src/store"); +import test from "tape"; +import Store from "../../../src/store"; -describe("hasOne", function() { - - var store; - - beforeEach(function() { - store = new Store(); - }); - - it("must return the correct type attribute", function () { - expect(Store.hasOne().type).toBe("has-one"); - expect(Store.hasOne("example").type).toBe("has-one"); - expect(Store.hasOne({}).type).toBe("has-one"); - expect(Store.hasOne("example", {}).type).toBe("has-one"); - }); +test("hasOne must return the correct type attribute", function (t) { + t.plan(4); + t.equal(Store.hasOne().type, "has-one"); + t.equal(Store.hasOne("example").type, "has-one"); + t.equal(Store.hasOne({}).type, "has-one"); + t.equal(Store.hasOne("example", {}).type, "has-one"); +}); - it("must return a deserialize function that maps to the relation described in the data property", function () { - store.define("categories", {}); - store.define("products", {}); - var field = Store.hasOne("category"); - var data = { - "type": "products", - "id": "1", - "relationships": { - "category": { - "data": { - "type": "categories", - "id": "2" - } +test("hasOne must return a deserialize function that maps to the relation described in the data property", function (t) { + var store = new Store(); + var field = Store.hasOne("category"); + var data = { + "type": "products", + "id": "1", + "relationships": { + "category": { + "data": { + "type": "categories", + "id": "2" } } - }; - expect(field.deserialize.call(store, data)).toBe(store.find("categories", "2")); - }); + } + }; + t.plan(1); + store.define("categories", {}); + store.define("products", {}); + t.equal(field.deserialize.call(store, data), store.find("categories", "2")); +}); - it("must return a deserialize function that uses the key param when the name isn't provided", function () { - store.define("categories", {}); - store.define("products", {}); - var field = Store.hasOne(); - var data = { - "type": "products", - "id": "1", - "relationships": { - "category": { - "data": { - "type": "categories", - "id": "2" - } +test("hasOne must return a deserialize function that uses the key param when the name isn't provided", function (t) { + var store = new Store(); + var field = Store.hasOne(); + var data = { + "type": "products", + "id": "1", + "relationships": { + "category": { + "data": { + "type": "categories", + "id": "2" } } - }; - expect(field.deserialize.call(store, data, "category")).toBe(store.find("categories", "2")); - }); + } + }; + t.plan(1); + store.define("categories", {}); + store.define("products", {}); + t.equal(field.deserialize.call(store, data, "category"), store.find("categories", "2")); +}); - it("must return a deserialize function that returns null when the relationship data field is null", function () { - var data = { - "type": "products", - "id": "1", - "relationships": { - "category": { - "data": null - } +test("hasOne must return a deserialize function that returns null when the relationship data field is null", function (t) { + var store = new Store(); + var data = { + "type": "products", + "id": "1", + "relationships": { + "category": { + "data": null } - }; - expect(Store.hasOne("category").deserialize.call(store, data)).toBeNull(); - expect(Store.hasOne().deserialize.call(store, data, "category")).toBeNull(); - }); + } + }; + t.plan(2); + t.equal(Store.hasOne("category").deserialize.call(store, data), null); + t.equal(Store.hasOne().deserialize.call(store, data, "category"), null); +}); - it("must return a deserialize function that returns undefined when the relationship data field is missing", function () { - var data = { - "type": "products", - "id": "1", - "relationships": { - "category": {} - } - }; - expect(Store.hasOne("category").deserialize.call(store, data)).toBeUndefined(); - expect(Store.hasOne().deserialize.call(store, data, "category")).toBeUndefined(); - }); +test("hasOne must return a deserialize function that returns undefined when the relationship data field is missing", function (t) { + var store = new Store(); + var data = { + "type": "products", + "id": "1", + "relationships": { + "category": {} + } + }; + t.plan(2); + t.equal(Store.hasOne("category").deserialize.call(store, data), undefined); + t.equal(Store.hasOne().deserialize.call(store, data, "category"), undefined); +}); - it("must return a deserialize function that returns undefined when the relationship type field is missing", function () { - var data = { - "type": "products", - "id": "1", - "relationships": {} - }; - expect(Store.hasOne("category").deserialize.call(store, data)).toBeUndefined(); - expect(Store.hasOne().deserialize.call(store, data, "category")).toBeUndefined(); - }); +test("hasOne must return a deserialize function that returns undefined when the relationship type field is missing", function (t) { + var store = new Store(); + var data = { + "type": "products", + "id": "1", + "relationships": {} + }; + t.plan(2); + t.equal(Store.hasOne("category").deserialize.call(store, data), undefined); + t.equal(Store.hasOne().deserialize.call(store, data, "category"), undefined); +}); - it("must return a deserialize function that returns undefined when the relationship field is missing", function () { - var data = { - "type": "products", - "id": "1" - }; - expect(Store.hasOne("category").deserialize.call(store, data)).toBeUndefined(); - expect(Store.hasOne().deserialize.call(store, data, "category")).toBeUndefined(); - }); +test("hasOne must return a deserialize function that returns undefined when the relationship field is missing", function (t) { + var store = new Store(); + var data = { + "type": "products", + "id": "1" + }; + t.plan(2); + t.equal(Store.hasOne("category").deserialize.call(store, data), undefined); + t.equal(Store.hasOne().deserialize.call(store, data, "category"), undefined); +}); - it("must return a deserialize function that passes on an inverse option", function () { - expect(Store.hasOne({ inverse: "foo" }).inverse).toBe("foo"); - expect(Store.hasOne("example", { inverse: "foo" }).inverse).toBe("foo"); - }); +test("hasOne must return a deserialize function that passes on an inverse option", function (t) { + t.plan(2); + t.equal(Store.hasOne({ inverse: "foo" }).inverse, "foo"); + t.equal(Store.hasOne("example", { inverse: "foo" }).inverse, "foo"); +}); - it("must throw an error a relationship's type hasn't been defined", function () { - store.define("products", {}); - var field = Store.hasOne(); - var data = { - "type": "products", - "id": "44", - "relationships": { - "category": { - "data": { "type": "categories", "id": "34" } - } +test("hasOne must throw an error a relationship's type hasn't been defined", function (t) { + var store = new Store(); + var field = Store.hasOne(); + var data = { + "type": "products", + "id": "44", + "relationships": { + "category": { + "data": { "type": "categories", "id": "34" } } - }; - expect(function () { - field.deserialize.call(store, data, "category"); - }).toThrowError("Unknown type 'categories'"); - }); - + } + }; + t.plan(1); + store.define("products", {}); + t.throws(function () { + field.deserialize.call(store, data, "category"); + }, /Unknown type 'categories'/); }); diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json deleted file mode 100644 index 8cdf058..0000000 --- a/spec/support/jasmine.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "spec_dir": "spec", - "spec_files": [ - "**/*-spec.js" - ], - "helpers": [ - "helpers/**/*.js" - ] -} -- 2.51.2 From 691d1e5c6167ddb0844ea98e519870eb30ac3c46 Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Thu, 27 Aug 2015 17:24:43 +0930 Subject: [PATCH 05/23] Setup separate browser & node tests. --- .travis.yml | 6 +++--- package.json | 9 +++------ scripts/test | 15 +++++++++++++++ spec/.eslintrc | 3 ++- spec/client-test.js | 5 ----- spec/client.js | 18 ++++++++++++++++++ spec/server-test.js | 1 - spec/{common-test.js => server.js} | 0 8 files changed, 41 insertions(+), 16 deletions(-) create mode 100755 scripts/test delete mode 100644 spec/client-test.js create mode 100644 spec/client.js delete mode 100644 spec/server-test.js rename spec/{common-test.js => server.js} (100%) diff --git a/.travis.yml b/.travis.yml index 4b64e2b..e0a44ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,6 @@ language: node_js node_js: - "0.12" - "0.11" - - "0.10" - - "iojs" - - "iojs-v1.0.4" +env: + - TARGET=browser + - TARGET=node diff --git a/package.json b/package.json index dcbd77e..701426a 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,8 @@ "repository": "haydn/json-api-store", "main": "dist/store.js", "scripts": { - "test": "npm run test-server && npm run test-client", - "test-pretty": "npm run test-server | node_modules/.bin/tap-spec && npm run test-client | node_modules/.bin/tap-spec || true", - "build": "node_modules/.bin/babel src/store.js -m umd --module-id Store", - "test-server": "node_modules/.bin/babel-node spec/server-test.js", - "test-client": "node_modules/.bin/browserify spec/client-test.js -t babelify | node_modules/.bin/tape-run" + "test": "scripts/test", + "build": "node_modules/.bin/babel src/store.js -m umd --module-id Store" }, "author": "Haydn Ewers", "license": "MIT", @@ -18,7 +15,7 @@ "babelify": "^6.2.0", "browserify": "^11.0.1", "sinon": "^1.16.1", - "tap-spec": "^4.1.0", + "tap-dot": "^1.0.0", "tape": "^4.2.0", "tape-run": "^1.1.0" }, diff --git a/scripts/test b/scripts/test new file mode 100755 index 0000000..461a2c1 --- /dev/null +++ b/scripts/test @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +if [ "$TARGET" = "browser" ]; then + node_modules/.bin/browserify spec/client.js -t babelify | node_modules/.bin/tape-run + exit $? +fi + +if [ "$TARGET" = "node" ]; then + node_modules/.bin/babel-node spec/server.js + exit $? +fi + +node_modules/.bin/babel-node spec/server.js | node_modules/.bin/tap-dot || true +node_modules/.bin/browserify spec/client.js -t babelify | node_modules/.bin/tape-run | node_modules/.bin/tap-dot || true +exit 0 diff --git a/spec/.eslintrc b/spec/.eslintrc index 412ae83..44f1e5f 100644 --- a/spec/.eslintrc +++ b/spec/.eslintrc @@ -1,6 +1,7 @@ { "env": { - "es6": true + "es6": true, + "node": true }, "ecmaFeatures": { "modules": true diff --git a/spec/client-test.js b/spec/client-test.js deleted file mode 100644 index 94d7a83..0000000 --- a/spec/client-test.js +++ /dev/null @@ -1,5 +0,0 @@ -import "./common-test"; -import "./adapters/ajax/create-spec"; -import "./adapters/ajax/destroy-spec"; -import "./adapters/ajax/load-spec"; -import "./adapters/ajax/update-spec"; diff --git a/spec/client.js b/spec/client.js new file mode 100644 index 0000000..c083d9d --- /dev/null +++ b/spec/client.js @@ -0,0 +1,18 @@ +import "./adapters/ajax/create-spec"; +import "./adapters/ajax/destroy-spec"; +import "./adapters/ajax/load-spec"; +import "./adapters/ajax/update-spec"; +import "./store/clud/create-spec"; +import "./store/clud/destroy-spec"; +import "./store/clud/load-spec"; +import "./store/clud/update-spec"; +import "./store/core/add-spec"; +import "./store/core/define-spec"; +import "./store/core/find-spec"; +import "./store/core/push-spec"; +import "./store/core/remove-spec"; +import "./store/events/off-spec"; +import "./store/events/on-spec"; +import "./store/fields/attr-spec"; +import "./store/fields/has-many-spec"; +import "./store/fields/has-one-spec"; diff --git a/spec/server-test.js b/spec/server-test.js deleted file mode 100644 index 8e06e99..0000000 --- a/spec/server-test.js +++ /dev/null @@ -1 +0,0 @@ -import "./common-test"; diff --git a/spec/common-test.js b/spec/server.js similarity index 100% rename from spec/common-test.js rename to spec/server.js -- 2.51.2 From ef88c75c650cae5dd780a9786e8b0ebaa1782c5a Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Fri, 28 Aug 2015 12:42:05 +0930 Subject: [PATCH 06/23] Improve the test scripts & other tooling. --- CONTRIBUTING.md | 42 ++++++++++++++++++++++++++++-------------- package.json | 11 ++++++++--- scripts/release | 21 +++++++++------------ scripts/test | 24 +++++++++++++++++++----- 4 files changed, 64 insertions(+), 34 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f5e8b34..646d631 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,19 @@ -## Tests +## Development Requirements + +- Node.js +- PhantomJS + +## Getting Started + +Clone the project and install NPM packages: + +``` +git clone git@github.com:haydn/json-api-store.git +cd json-api-store +npm install +``` + +## Running Tests You can run tests once-off with NPM: @@ -6,34 +21,33 @@ You can run tests once-off with NPM: npm test ``` -Alternatively, you can run tests in watch mode using -[nodemon](http://nodemon.io): +Alternatively, you can run tests in watch mode: ``` -nodemon node_modules/jasmine/bin/jasmine.js +npm start ``` -## Documentation +## Generating Documentation -You can generate the documentation with [esdoc](https://esdoc.org/): +You can regenerate the documentation with: ``` -esdoc -c esdoc.json +npm run docs ``` -## Building +## Building Distribution -You can rebuild the the output from the source using -[babel](https://babeljs.io): +You can rebuild the the output from the source using: ``` -babel src/store.js -m umd --module-id Store --compact true --no-comments -o dist/store.js +npm run build ``` -## Releases +## Making Releases -You can make a new release using the script: +There's a script available for making releases. Without the required +permissions, you won't get you very far, but if you're curious here it is: ``` -script/release +npm run release ``` diff --git a/package.json b/package.json index 701426a..8d93de3 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,11 @@ "repository": "haydn/json-api-store", "main": "dist/store.js", "scripts": { - "test": "scripts/test", - "build": "node_modules/.bin/babel src/store.js -m umd --module-id Store" + "build": "./node_modules/.bin/babel src/store.js -m umd --module-id Store --compact --no-comments -o dist/store.js -s", + "docs": "./node_modules/.bin/esdoc -c esdoc.json", + "release": "./scripts/release", + "start": "./scripts/test --watch", + "test": "./scripts/test" }, "author": "Haydn Ewers", "license": "MIT", @@ -14,8 +17,10 @@ "babel": "^5.8.21", "babelify": "^6.2.0", "browserify": "^11.0.1", + "chokidar-cli": "^1.0.1", + "esdoc": "^0.2.2", "sinon": "^1.16.1", - "tap-dot": "^1.0.0", + "tap-spec": "^4.1.0", "tape": "^4.2.0", "tape-run": "^1.1.0" }, diff --git a/scripts/release b/scripts/release index 2615c8e..312e685 100755 --- a/scripts/release +++ b/scripts/release @@ -1,13 +1,11 @@ #!/usr/bin/env bash -if [ $(git branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/') != "master" ] -then +if [ $(git branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/') != "master" ]; then echo >&2 "Looks like you're not on the master branch. Checkout master and run this script." exit 1 fi -if [[ -n $(git status --porcelain 2> /dev/null) ]] -then +if [[ -n $(git status --porcelain 2> /dev/null) ]]; then echo >&2 "Looks like you're in a dirty head state. Clean-up un-committed files before and run this script again." exit 1 fi @@ -21,8 +19,7 @@ npm install echo "$ npm test" npm test -if [ $? -ne 0 ] -then +if [ $? -ne 0 ]; then echo >&2 "Looks like there are failing tests. Make sure all tests pass and run this script again." exit 1 fi @@ -30,8 +27,7 @@ fi echo "$ eslint --quiet src" eslint --quiet src -if [ $? -ne 0 ] -then +if [ $? -ne 0 ]; then echo >&2 "Looks like there are linting errors. Make sure all errors are cleaned-up and run this script again." exit 1 fi @@ -42,9 +38,9 @@ git tag -l read -p "Enter the version number for this release (eg '1.4.5'): " version echo "Building docs..." -esdoc -c esdoc.json +npm run docs echo "Building dist..." -babel src/store.js -m umd --module-id Store --compact true --no-comments -o dist/store.js +npm run build echo "Updating NPM and Bower manifests..." sed -i.bak -E "s/\"version\": \"[0-9]+\.[0-9]+\.[0-9]+\"/\"version\": \"$version\"/" package.json rm package.json.bak @@ -52,8 +48,9 @@ sed -i.bak -E "s/\"version\": \"[0-9]+\.[0-9]+\.[0-9]+\"/\"version\": \"$version rm bower.json.bak read -p "Are you sure you want to release v$version? (yes/no) " confirm -if [ $confirm != "yes" ] -then + +if [ $confirm != "yes" ]; then + echo "Aborting." exit 0 fi diff --git a/scripts/test b/scripts/test index 461a2c1..71c5b04 100755 --- a/scripts/test +++ b/scripts/test @@ -1,15 +1,29 @@ #!/usr/bin/env bash if [ "$TARGET" = "browser" ]; then - node_modules/.bin/browserify spec/client.js -t babelify | node_modules/.bin/tape-run + ./node_modules/.bin/browserify spec/client.js -t babelify | node_modules/.bin/tape-run exit $? fi if [ "$TARGET" = "node" ]; then - node_modules/.bin/babel-node spec/server.js + ./node_modules/.bin/babel-node spec/server.js exit $? fi -node_modules/.bin/babel-node spec/server.js | node_modules/.bin/tap-dot || true -node_modules/.bin/browserify spec/client.js -t babelify | node_modules/.bin/tape-run | node_modules/.bin/tap-dot || true -exit 0 +result=1 + +if [ "$1" = "--watch" ]; then + ./node_modules/.bin/chokidar './src/**/*.js' './spec/**/*.js' -c './scripts/test' + result=$? +else + echo "Testing in node:" + ./node_modules/.bin/babel-node spec/server.js | node_modules/.bin/tap-spec + result=$? + if [ $result -eq 0 ]; then + echo "Testing in browser:" + ./node_modules/.bin/browserify spec/client.js -t babelify | node_modules/.bin/tape-run | node_modules/.bin/tap-spec + result=$? + fi +fi + +exit $result -- 2.51.2 From 45d5f66b93e63f0383e2e4494f7b9ef3f0491b77 Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Fri, 28 Aug 2015 17:49:50 +0930 Subject: [PATCH 07/23] Add load method to AjaxAdapter. Add tests to store CLUD methods. --- spec/adapters/ajax/create-spec.js | 36 ++++++++++++- spec/adapters/ajax/load-spec.js | 84 ++++++++++++++++++++++++++++++- spec/store/clud/create-spec.js | 13 +++++ spec/store/clud/destroy-spec.js | 12 +++++ spec/store/clud/load-spec.js | 12 +++++ spec/store/clud/update-spec.js | 13 +++++ src/ajax-adapter.js | 52 +++++++++++++++++++ src/store.js | 38 ++++++++++---- 8 files changed, 247 insertions(+), 13 deletions(-) create mode 100644 src/ajax-adapter.js diff --git a/spec/adapters/ajax/create-spec.js b/spec/adapters/ajax/create-spec.js index 18bc4eb..f949d24 100644 --- a/spec/adapters/ajax/create-spec.js +++ b/spec/adapters/ajax/create-spec.js @@ -1,2 +1,34 @@ -// import test from "tape"; -// import Store from "../../../src/store"; +import test from "tape"; +import sinon from "sinon"; +import Store from "../../../src/store"; + +test.skip("create must post a resource to the server and add it to the store", function (t) { + var server = sinon.fakeServer.create({ autoRespond: true }); + var adapter = new Store.AjaxAdapter(); + var store = new Store(adapter); + t.plan(2); + t.timeoutAfter(1000); + store.define("products", { + title: Store.attr() + }); + server.respondWith("POST", "/products", [ + 201, + { + "Content-Type": "application/vnd.api+json" + }, + JSON.stringify({ + data: { + type: "products", + id: "9", + attributes: { + title: "My Book" + } + } + }) + ]); + store.create("products", { title: "My Book" }, function (product) { + t.equal(product.title, "My Book"); + t.equal(store.find("products", "9").title, "My Book"); + }); + server.restore(); +}); diff --git a/spec/adapters/ajax/load-spec.js b/spec/adapters/ajax/load-spec.js index 18bc4eb..0f52319 100644 --- a/spec/adapters/ajax/load-spec.js +++ b/spec/adapters/ajax/load-spec.js @@ -1,2 +1,82 @@ -// import test from "tape"; -// import Store from "../../../src/store"; +import test from "tape"; +import sinon from "sinon"; +import Store from "../../../src/store"; + +test("load must fetch a single resource from the server and add it to the store", function (t) { + var server = sinon.fakeServer.create({ autoRespond: true }); + var adapter = new Store.AjaxAdapter(); + var store = new Store(adapter); + t.plan(2); + t.timeoutAfter(1000); + store.define("products", { + title: Store.attr() + }); + server.respondWith("GET", "/products/12", [ + 200, + { + "Content-Type": "application/vnd.api+json" + }, + JSON.stringify({ + data: { + type: "products", + id: "12", + attributes: { + title: "An Awesome Book" + } + } + }) + ]); + store.load("products", "12", function (product) { + t.equal(product.title, "An Awesome Book"); + t.equal(store.find("products", "12").title, "An Awesome Book"); + }); + server.restore(); +}); + +test("load must fetch a collection of resources from the server and add them to the store", function (t) { + var server = sinon.fakeServer.create({ autoRespond: true }); + var adapter = new Store.AjaxAdapter(); + var store = new Store(adapter); + t.plan(3); + t.timeoutAfter(1000); + store.define("products", { + title: Store.attr() + }); + server.respondWith("GET", "/products", [ + 200, + { + "Content-Type": "application/vnd.api+json" + }, + JSON.stringify({ + data: [ + { + type: "products", + id: "2", + attributes: { + title: "A Book" + } + }, + { + type: "products", + id: "4", + attributes: { + title: "B Book" + } + }, + { + type: "products", + id: "7", + attributes: { + title: "C Book" + } + } + ] + }) + ]); + store.load("products", 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" ]); + }); + server.restore(); +}); diff --git a/spec/store/clud/create-spec.js b/spec/store/clud/create-spec.js index 05c614b..cbd6b2c 100644 --- a/spec/store/clud/create-spec.js +++ b/spec/store/clud/create-spec.js @@ -1,4 +1,5 @@ import test from "tape"; +import sinon from "sinon"; import Store from "../../../src/store"; test("create must throw an error if it is called when there isn't an adapter", function (t) { @@ -8,3 +9,15 @@ test("create must throw an error if it is called when there isn't an adapter", f store.create(); }, /Adapter missing\. Specify an adapter when creating the store: `var store = new Store\(adapter\);`/); }); + +test("create must call the create method prodvided by the adapter", function (t) { + var adatper = { create: sinon.spy() }; + var store = new Store(adatper); + var a = {}; + var cb = function () {}; + t.plan(2); + t.doesNotThrow(function () { + store.create("foo", a, cb); + }, "should not throw an error"); + t.ok(adatper.create.calledWith(store, "foo", a, cb), "should call adapter with the same params"); +}); diff --git a/spec/store/clud/destroy-spec.js b/spec/store/clud/destroy-spec.js index 3c7eb16..02fea76 100644 --- a/spec/store/clud/destroy-spec.js +++ b/spec/store/clud/destroy-spec.js @@ -1,4 +1,5 @@ import test from "tape"; +import sinon from "sinon"; import Store from "../../../src/store"; test("destroy must throw an error if it is called when there isn't an adapter", function (t) { @@ -8,3 +9,14 @@ test("destroy must throw an error if it is called when there isn't an adapter", store.destroy(); }, /Adapter missing\. Specify an adapter when creating the store: `var store = new Store\(adapter\);`/); }); + +test("destroy must call the destroy method prodvided by the adapter", function (t) { + var adatper = { destroy: sinon.spy() }; + var store = new Store(adatper); + var cb = function () {}; + t.plan(2); + t.doesNotThrow(function () { + store.destroy("foo", "1", cb); + }, "should not throw an error"); + t.ok(adatper.destroy.calledWith(store, "foo", "1", cb), "should call adapter with the same params"); +}); diff --git a/spec/store/clud/load-spec.js b/spec/store/clud/load-spec.js index 0aac5a7..92153ce 100644 --- a/spec/store/clud/load-spec.js +++ b/spec/store/clud/load-spec.js @@ -1,4 +1,5 @@ import test from "tape"; +import sinon from "sinon"; import Store from "../../../src/store"; test("load must throw an error if it is called when there isn't an adapter", function (t) { @@ -8,3 +9,14 @@ test("load must throw an error if it is called when there isn't an adapter", fun store.load(); }, /Adapter missing\. Specify an adapter when creating the store: `var store = new Store\(adapter\);`/); }); + +test("load must call the load method prodvided by the adapter", function (t) { + var adatper = { load: sinon.spy() }; + var store = new Store(adatper); + var cb = function () {}; + t.plan(2); + t.doesNotThrow(function () { + store.load("foo", "1", cb); + }, "should not throw an error"); + t.ok(adatper.load.calledWith(store, "foo", "1", cb), "should call adapter with the same params"); +}); diff --git a/spec/store/clud/update-spec.js b/spec/store/clud/update-spec.js index d2d2e18..2ffaf97 100644 --- a/spec/store/clud/update-spec.js +++ b/spec/store/clud/update-spec.js @@ -1,4 +1,5 @@ import test from "tape"; +import sinon from "sinon"; import Store from "../../../src/store"; test("update must throw an error if update is called when there isn't an adapter", function (t) { @@ -8,3 +9,15 @@ test("update must throw an error if update is called when there isn't an adapter store.update(); }, /Adapter missing\. Specify an adapter when creating the store: `var store = new Store\(adapter\);`/); }); + +test("update must call the update method prodvided by the adapter", function (t) { + var adatper = { update: sinon.spy() }; + var store = new Store(adatper); + var a = {}; + var cb = function () {}; + t.plan(2); + t.doesNotThrow(function () { + store.update("foo", "1", a, cb); + }, "should not throw an error"); + t.ok(adatper.update.calledWith(store, "foo", "1", a, cb), "should call adapter with the same params"); +}); diff --git a/src/ajax-adapter.js b/src/ajax-adapter.js new file mode 100644 index 0000000..bfe48ac --- /dev/null +++ b/src/ajax-adapter.js @@ -0,0 +1,52 @@ +export default class AjaxAdapter { + + // create(store, type, data, callback) { + // + // let request = new XMLHttpRequest(); + // + // request.open('POST', `/${type}`, true); + // + // request.onload = function () { + // var data = JSON.parse(request.responseText); + // store.push(data); + // callback(store.find(type, data.id)); + // }; + // + // // we need to convert to data here. + // + // request.send({ + // data: JSON.stringify(data) + // }); + // + // } + + load(store, type, id, options, callback) { + + if (id && {}.toString.call(id) === '[object Function]') { + this.load(store, type, null, null, id); + } else if (options && {}.toString.call(options) === '[object Function]') { + this.load(store, type, id, null, options); + } else if (id && typeof id === "object") { + this.load(store, type, null, id, callback); + } else { + + let request = new XMLHttpRequest(); + let url; + + options = options || {}; + url = id ? `/${type}/${id}` : `/${type}`; + + request.open('GET', url, true); + + request.onload = function () { + store.push(JSON.parse(request.responseText)); + callback(store.find(type, id)); + }; + + request.send(); + + } + + } + +} diff --git a/src/store.js b/src/store.js index a441edc..d744fc1 100644 --- a/src/store.js +++ b/src/store.js @@ -1,4 +1,5 @@ import "array.prototype.find"; +import AjaxAdapter from "./ajax-adapter"; export default class Store { @@ -88,7 +89,8 @@ export default class Store { } } - constructor() { + constructor(adapter) { + this._adapter = adapter; this._collectionListeners = { "added": {}, "updated": {}, "removed": {} }; this._data = {}; this._resourceListeners = { "added": {}, "updated": {}, "removed": {} }; @@ -129,8 +131,12 @@ export default class Store { } } - create() { - throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + create(type, data, callback) { + if (this._adapter) { + this._adapter.create(this, type, data, callback); + } else { + throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + } } /** @@ -153,8 +159,12 @@ export default class Store { }); } - destroy() { - throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + destroy(type, id, callback) { + if (this._adapter) { + this._adapter.destroy(this, type, id, callback); + } else { + throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + } } /** @@ -202,8 +212,12 @@ export default class Store { } } - load() { - throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + load(type, id, callback) { + if (this._adapter) { + this._adapter.load(this, type, id, callback); + } else { + throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + } } /** @@ -340,8 +354,12 @@ export default class Store { } } - update() { - throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + update(type, id, data, callback) { + if (this._adapter) { + this._adapter.update(this, type, id, data, callback); + } else { + throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); + } } _addField(object, resource, definition, fieldName) { @@ -450,3 +468,5 @@ export default class Store { } } + +Store.AjaxAdapter = AjaxAdapter; -- 2.51.2 From 95b1344fd3758a09b35d61e91ef66f021d29c304 Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Tue, 1 Sep 2015 10:57:06 +0930 Subject: [PATCH 08/23] Improve the test runner script. --- scripts/test | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/scripts/test b/scripts/test index 71c5b04..e7d6d9e 100755 --- a/scripts/test +++ b/scripts/test @@ -10,20 +10,16 @@ if [ "$TARGET" = "node" ]; then exit $? fi -result=1 - if [ "$1" = "--watch" ]; then ./node_modules/.bin/chokidar './src/**/*.js' './spec/**/*.js' -c './scripts/test' - result=$? else - echo "Testing in node:" + set -o pipefail + echo "NODE:" ./node_modules/.bin/babel-node spec/server.js | node_modules/.bin/tap-spec - result=$? - if [ $result -eq 0 ]; then - echo "Testing in browser:" - ./node_modules/.bin/browserify spec/client.js -t babelify | node_modules/.bin/tape-run | node_modules/.bin/tap-spec - result=$? + if [ $? -eq 0 ]; then + echo "BROWSER:" + ./node_modules/.bin/browserify spec/client.js -d -t babelify | node_modules/.bin/tape-run | node_modules/.bin/tap-spec fi fi -exit $result +exit $? -- 2.51.2 From a4cf271e997027c55022d1b3e1d39510967bf67e Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Thu, 3 Sep 2015 10:01:55 +0930 Subject: [PATCH 09/23] Use tape-catch in tests to handle errors. --- package.json | 1 + spec/adapters/ajax/create-spec.js | 2 +- spec/adapters/ajax/destroy-spec.js | 2 +- spec/adapters/ajax/load-spec.js | 2 +- spec/adapters/ajax/update-spec.js | 2 -- spec/store/clud/create-spec.js | 2 +- spec/store/clud/destroy-spec.js | 2 +- spec/store/clud/load-spec.js | 2 +- spec/store/clud/update-spec.js | 2 +- spec/store/core/add-spec.js | 2 +- spec/store/core/define-spec.js | 2 +- spec/store/core/find-spec.js | 2 +- spec/store/core/push-spec.js | 2 +- spec/store/core/remove-spec.js | 2 +- spec/store/events/off-spec.js | 2 +- spec/store/events/on-spec.js | 2 +- spec/store/fields/attr-spec.js | 2 +- spec/store/fields/has-many-spec.js | 2 +- spec/store/fields/has-one-spec.js | 2 +- 19 files changed, 18 insertions(+), 19 deletions(-) delete mode 100644 spec/adapters/ajax/update-spec.js diff --git a/package.json b/package.json index 8d93de3..0a65f42 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "sinon": "^1.16.1", "tap-spec": "^4.1.0", "tape": "^4.2.0", + "tape-catch": "^1.0.4", "tape-run": "^1.1.0" }, "dependencies": { diff --git a/spec/adapters/ajax/create-spec.js b/spec/adapters/ajax/create-spec.js index f949d24..58e4a4b 100644 --- a/spec/adapters/ajax/create-spec.js +++ b/spec/adapters/ajax/create-spec.js @@ -1,4 +1,4 @@ -import test from "tape"; +import test from "tape-catch"; import sinon from "sinon"; import Store from "../../../src/store"; diff --git a/spec/adapters/ajax/destroy-spec.js b/spec/adapters/ajax/destroy-spec.js index 18bc4eb..c17e3f1 100644 --- a/spec/adapters/ajax/destroy-spec.js +++ b/spec/adapters/ajax/destroy-spec.js @@ -1,2 +1,2 @@ -// import test from "tape"; +// import test from "tape-catch"; // import Store from "../../../src/store"; diff --git a/spec/adapters/ajax/load-spec.js b/spec/adapters/ajax/load-spec.js index 0f52319..81b8ac2 100644 --- a/spec/adapters/ajax/load-spec.js +++ b/spec/adapters/ajax/load-spec.js @@ -1,4 +1,4 @@ -import test from "tape"; +import test from "tape-catch"; import sinon from "sinon"; import Store from "../../../src/store"; diff --git a/spec/adapters/ajax/update-spec.js b/spec/adapters/ajax/update-spec.js deleted file mode 100644 index 18bc4eb..0000000 --- a/spec/adapters/ajax/update-spec.js +++ /dev/null @@ -1,2 +0,0 @@ -// import test from "tape"; -// import Store from "../../../src/store"; diff --git a/spec/store/clud/create-spec.js b/spec/store/clud/create-spec.js index cbd6b2c..f9bf17a 100644 --- a/spec/store/clud/create-spec.js +++ b/spec/store/clud/create-spec.js @@ -1,4 +1,4 @@ -import test from "tape"; +import test from "tape-catch"; import sinon from "sinon"; import Store from "../../../src/store"; diff --git a/spec/store/clud/destroy-spec.js b/spec/store/clud/destroy-spec.js index 02fea76..44862d9 100644 --- a/spec/store/clud/destroy-spec.js +++ b/spec/store/clud/destroy-spec.js @@ -1,4 +1,4 @@ -import test from "tape"; +import test from "tape-catch"; import sinon from "sinon"; import Store from "../../../src/store"; diff --git a/spec/store/clud/load-spec.js b/spec/store/clud/load-spec.js index 92153ce..e6a41a7 100644 --- a/spec/store/clud/load-spec.js +++ b/spec/store/clud/load-spec.js @@ -1,4 +1,4 @@ -import test from "tape"; +import test from "tape-catch"; import sinon from "sinon"; import Store from "../../../src/store"; diff --git a/spec/store/clud/update-spec.js b/spec/store/clud/update-spec.js index 2ffaf97..0e257e3 100644 --- a/spec/store/clud/update-spec.js +++ b/spec/store/clud/update-spec.js @@ -1,4 +1,4 @@ -import test from "tape"; +import test from "tape-catch"; import sinon from "sinon"; import Store from "../../../src/store"; diff --git a/spec/store/core/add-spec.js b/spec/store/core/add-spec.js index 1a79aa1..3518cd4 100644 --- a/spec/store/core/add-spec.js +++ b/spec/store/core/add-spec.js @@ -1,4 +1,4 @@ -import test from "tape"; +import test from "tape-catch"; import Store from "../../../src/store"; test("add must add a resource to the store", function (t) { diff --git a/spec/store/core/define-spec.js b/spec/store/core/define-spec.js index c187998..67c6410 100644 --- a/spec/store/core/define-spec.js +++ b/spec/store/core/define-spec.js @@ -1,4 +1,4 @@ -import test from "tape"; +import test from "tape-catch"; import Store from "../../../src/store"; test("define must accept pseudonyms", function (t) { diff --git a/spec/store/core/find-spec.js b/spec/store/core/find-spec.js index 62b621b..ad19daf 100644 --- a/spec/store/core/find-spec.js +++ b/spec/store/core/find-spec.js @@ -1,4 +1,4 @@ -import test from "tape"; +import test from "tape-catch"; import Store from "../../../src/store"; test("find must, when an id is provided, return an object with 'type' and 'id' properties", function (t) { diff --git a/spec/store/core/push-spec.js b/spec/store/core/push-spec.js index df8499d..3d61932 100644 --- a/spec/store/core/push-spec.js +++ b/spec/store/core/push-spec.js @@ -1,4 +1,4 @@ -import test from "tape"; +import test from "tape-catch"; import sinon from "sinon"; import Store from "../../../src/store"; diff --git a/spec/store/core/remove-spec.js b/spec/store/core/remove-spec.js index e83d26e..f4b0c6f 100644 --- a/spec/store/core/remove-spec.js +++ b/spec/store/core/remove-spec.js @@ -1,4 +1,4 @@ -import test from "tape"; +import test from "tape-catch"; import sinon from "sinon"; import Store from "../../../src/store"; diff --git a/spec/store/events/off-spec.js b/spec/store/events/off-spec.js index 3e03dc4..a8c751e 100644 --- a/spec/store/events/off-spec.js +++ b/spec/store/events/off-spec.js @@ -1,4 +1,4 @@ -import test from "tape"; +import test from "tape-catch"; import sinon from "sinon"; import Store from "../../../src/store"; diff --git a/spec/store/events/on-spec.js b/spec/store/events/on-spec.js index 80ca626..9114671 100644 --- a/spec/store/events/on-spec.js +++ b/spec/store/events/on-spec.js @@ -1,4 +1,4 @@ -import test from "tape"; +import test from "tape-catch"; import sinon from "sinon"; import Store from "../../../src/store"; diff --git a/spec/store/fields/attr-spec.js b/spec/store/fields/attr-spec.js index d94c4cd..0939db2 100644 --- a/spec/store/fields/attr-spec.js +++ b/spec/store/fields/attr-spec.js @@ -1,4 +1,4 @@ -import test from "tape"; +import test from "tape-catch"; import Store from "../../../src/store"; test("attr must return the correct type attribute", function (t) { diff --git a/spec/store/fields/has-many-spec.js b/spec/store/fields/has-many-spec.js index 2fe9060..5729671 100644 --- a/spec/store/fields/has-many-spec.js +++ b/spec/store/fields/has-many-spec.js @@ -1,4 +1,4 @@ -import test from "tape"; +import test from "tape-catch"; import Store from "../../../src/store"; test("hasMany must return the correct type attribute", function (t) { diff --git a/spec/store/fields/has-one-spec.js b/spec/store/fields/has-one-spec.js index 0b95dd3..375cba8 100644 --- a/spec/store/fields/has-one-spec.js +++ b/spec/store/fields/has-one-spec.js @@ -1,4 +1,4 @@ -import test from "tape"; +import test from "tape-catch"; import Store from "../../../src/store"; test("hasOne must return the correct type attribute", function (t) { -- 2.51.2 From d9309e8d2db87d0d6ef39f70fde6fab0148f6c43 Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Thu, 3 Sep 2015 10:02:37 +0930 Subject: [PATCH 10/23] Use faucet instead on tap-spec for test reporting. --- package.json | 2 +- scripts/test | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 0a65f42..60e258a 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "browserify": "^11.0.1", "chokidar-cli": "^1.0.1", "esdoc": "^0.2.2", + "faucet": "0.0.1", "sinon": "^1.16.1", - "tap-spec": "^4.1.0", "tape": "^4.2.0", "tape-catch": "^1.0.4", "tape-run": "^1.1.0" diff --git a/scripts/test b/scripts/test index e7d6d9e..c4bda5a 100755 --- a/scripts/test +++ b/scripts/test @@ -15,10 +15,10 @@ if [ "$1" = "--watch" ]; then else set -o pipefail echo "NODE:" - ./node_modules/.bin/babel-node spec/server.js | node_modules/.bin/tap-spec + ./node_modules/.bin/babel-node spec/server.js | node_modules/.bin/faucet if [ $? -eq 0 ]; then echo "BROWSER:" - ./node_modules/.bin/browserify spec/client.js -d -t babelify | node_modules/.bin/tape-run | node_modules/.bin/tap-spec + ./node_modules/.bin/browserify spec/client.js -d -t babelify | node_modules/.bin/tape-run | node_modules/.bin/faucet fi fi -- 2.51.2 From 4b2bcda5e166e8450142adcb6c33e262b775d67c Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Thu, 3 Sep 2015 10:09:45 +0930 Subject: [PATCH 11/23] Remove the `npm start` script for running tests. --- CONTRIBUTING.md | 2 +- package.json | 1 - scripts/test | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 646d631..fb1de34 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ npm test Alternatively, you can run tests in watch mode: ``` -npm start +npm test -- -w ``` ## Generating Documentation diff --git a/package.json b/package.json index 60e258a..89851e5 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "build": "./node_modules/.bin/babel src/store.js -m umd --module-id Store --compact --no-comments -o dist/store.js -s", "docs": "./node_modules/.bin/esdoc -c esdoc.json", "release": "./scripts/release", - "start": "./scripts/test --watch", "test": "./scripts/test" }, "author": "Haydn Ewers", diff --git a/scripts/test b/scripts/test index c4bda5a..37953da 100755 --- a/scripts/test +++ b/scripts/test @@ -10,7 +10,7 @@ if [ "$TARGET" = "node" ]; then exit $? fi -if [ "$1" = "--watch" ]; then +if [ "$1" = "--watch" ] || [ "$1" = "-w" ]; then ./node_modules/.bin/chokidar './src/**/*.js' './spec/**/*.js' -c './scripts/test' else set -o pipefail -- 2.51.2 From fc4276b2d4bd473ce73374fe53223240321b7b22 Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Thu, 3 Sep 2015 10:52:29 +0930 Subject: [PATCH 12/23] Add missing ajax/update test. --- spec/adapters/ajax/update-spec.js | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 spec/adapters/ajax/update-spec.js diff --git a/spec/adapters/ajax/update-spec.js b/spec/adapters/ajax/update-spec.js new file mode 100644 index 0000000..c17e3f1 --- /dev/null +++ b/spec/adapters/ajax/update-spec.js @@ -0,0 +1,2 @@ +// import test from "tape-catch"; +// import Store from "../../../src/store"; -- 2.51.2 From c87cba826bf87522ac2617711bb891722d911a92 Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Thu, 3 Sep 2015 11:49:31 +0930 Subject: [PATCH 13/23] Add create, update and destroy methods on the AJAX Adapter. --- spec/adapters/ajax/create-spec.js | 10 ++++- spec/adapters/ajax/destroy-spec.js | 30 ++++++++++++- spec/adapters/ajax/load-spec.js | 41 ++++++++++++++++-- spec/adapters/ajax/update-spec.js | 35 ++++++++++++++- spec/client.js | 15 +------ spec/server.js | 15 +------ spec/shared.js | 15 +++++++ spec/store/clud/create-spec.js | 4 +- spec/store/clud/destroy-spec.js | 5 ++- spec/store/clud/update-spec.js | 4 +- spec/store/core/convert-spec.js | 25 +++++++++++ spec/store/core/define-spec.js | 8 ++++ spec/store/fields/attr-spec.js | 39 ++++++++++++++++- src/ajax-adapter.js | 69 ++++++++++++++++++++++-------- src/store.js | 51 ++++++++++++++++------ 15 files changed, 289 insertions(+), 77 deletions(-) create mode 100644 spec/shared.js create mode 100644 spec/store/core/convert-spec.js diff --git a/spec/adapters/ajax/create-spec.js b/spec/adapters/ajax/create-spec.js index 58e4a4b..ecf528f 100644 --- a/spec/adapters/ajax/create-spec.js +++ b/spec/adapters/ajax/create-spec.js @@ -2,7 +2,7 @@ import test from "tape-catch"; import sinon from "sinon"; import Store from "../../../src/store"; -test.skip("create must post a resource to the server and add it to the store", function (t) { +test("create must post a resource to the server and add it to the store on success", function (t) { var server = sinon.fakeServer.create({ autoRespond: true }); var adapter = new Store.AjaxAdapter(); var store = new Store(adapter); @@ -26,9 +26,15 @@ test.skip("create must post a resource to the server and add it to the store", f } }) ]); - store.create("products", { title: "My Book" }, function (product) { + store.create({ type: "products", title: "My Book" }, function (product) { t.equal(product.title, "My Book"); t.equal(store.find("products", "9").title, "My Book"); }); server.restore(); }); + +test.skip("must throw an error if resource is missing a 'type' property"); + +test.skip("must call the error callback if an undefined type is included"); + +test.skip("must call the error callback if the server responds with a non-2xx code"); diff --git a/spec/adapters/ajax/destroy-spec.js b/spec/adapters/ajax/destroy-spec.js index c17e3f1..4161ce1 100644 --- a/spec/adapters/ajax/destroy-spec.js +++ b/spec/adapters/ajax/destroy-spec.js @@ -1,2 +1,28 @@ -// import test from "tape-catch"; -// import Store from "../../../src/store"; +import test from "tape-catch"; +import sinon from "sinon"; +import Store from "../../../src/store"; + +test("destroy must delete a resource from the server and remove it from the store on success", function (t) { + var server = sinon.fakeServer.create({ autoRespond: true }); + var adapter = new Store.AjaxAdapter(); + var store = new Store(adapter); + t.plan(2); + t.timeoutAfter(1000); + store.define("products", {}); + server.respondWith("DELETE", "/products/6", [ + 204, + { + "Content-Type": "application/vnd.api+json" + }, + "" + ]); + store.add({ + type: "products", + id: "6" + }); + t.equal(store.find("products").length, 1); + store.destroy(store.find("products", "6"), function () { + t.equal(store.find("products").length, 0); + }); + server.restore(); +}); diff --git a/spec/adapters/ajax/load-spec.js b/spec/adapters/ajax/load-spec.js index 81b8ac2..169c2d0 100644 --- a/spec/adapters/ajax/load-spec.js +++ b/spec/adapters/ajax/load-spec.js @@ -6,11 +6,15 @@ test("load must fetch a single resource from the server and add it to the store" var server = sinon.fakeServer.create({ autoRespond: true }); var adapter = new Store.AjaxAdapter(); var store = new Store(adapter); - t.plan(2); + t.plan(5); t.timeoutAfter(1000); store.define("products", { - title: Store.attr() + title: Store.attr(), + category: Store.hasOne(), + comments: Store.hasMany() }); + store.define("categories", {}); + store.define("comments", {}); server.respondWith("GET", "/products/12", [ 200, { @@ -22,13 +26,36 @@ test("load must fetch a single resource from the server and add it to the store" id: "12", attributes: { title: "An Awesome Book" + }, + relationships: { + category: { + data: { + id: "6", + type: "categories" + } + }, + comments: { + data: [ + { + id: "2", + type: "comments" + }, + { + id: "4", + type: "comments" + } + ] + } } } }) ]); store.load("products", "12", function (product) { - t.equal(product.title, "An Awesome Book"); + 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")); + t.deepEqual(store.find("products", "12").comments.map(c => c.id).sort(), [ "2", "4" ]); + t.deepEqual(store.find("products", "12").comments.map(c => c.type).sort(), [ "comments", "comments" ]); }); server.restore(); }); @@ -77,6 +104,14 @@ test("load must fetch a collection of resources from the server and add them to 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" ]); + }, function (error) { + t.fail(error); }); server.restore(); }); + +test.skip("must call the error callback if an undefined type is included"); + +test.skip("must call the error callback if the server responds with a non-2xx code"); + +test.skip("must return a promise if no callbacks are provided"); diff --git a/spec/adapters/ajax/update-spec.js b/spec/adapters/ajax/update-spec.js index c17e3f1..6311759 100644 --- a/spec/adapters/ajax/update-spec.js +++ b/spec/adapters/ajax/update-spec.js @@ -1,2 +1,33 @@ -// import test from "tape-catch"; -// import Store from "../../../src/store"; +import test from "tape-catch"; +import sinon from "sinon"; +import Store from "../../../src/store"; + +test("update must update a resource on the server and add reflect the changes in the store on success", function (t) { + var server = sinon.fakeServer.create({ autoRespond: true }); + var adapter = new Store.AjaxAdapter(); + var store = new Store(adapter); + t.plan(2); + t.timeoutAfter(1000); + store.define("products", { + title: Store.attr() + }); + server.respondWith("PATCH", "/products/9", [ + 204, + { + "Content-Type": "application/vnd.api+json" + }, + "" + ]); + store.add({ + type: "products", + id: "9", + attributes: { + title: "My Book" + } + }); + store.update({ type: "products", id: "9", title: "My Book!" }, function (product) { + t.equal(product.title, "My Book!"); + t.equal(store.find("products", "9").title, "My Book!"); + }); + server.restore(); +}); diff --git a/spec/client.js b/spec/client.js index c083d9d..7ae8b44 100644 --- a/spec/client.js +++ b/spec/client.js @@ -2,17 +2,4 @@ import "./adapters/ajax/create-spec"; import "./adapters/ajax/destroy-spec"; import "./adapters/ajax/load-spec"; import "./adapters/ajax/update-spec"; -import "./store/clud/create-spec"; -import "./store/clud/destroy-spec"; -import "./store/clud/load-spec"; -import "./store/clud/update-spec"; -import "./store/core/add-spec"; -import "./store/core/define-spec"; -import "./store/core/find-spec"; -import "./store/core/push-spec"; -import "./store/core/remove-spec"; -import "./store/events/off-spec"; -import "./store/events/on-spec"; -import "./store/fields/attr-spec"; -import "./store/fields/has-many-spec"; -import "./store/fields/has-one-spec"; +import "./shared"; diff --git a/spec/server.js b/spec/server.js index 14e2216..d44247b 100644 --- a/spec/server.js +++ b/spec/server.js @@ -1,14 +1 @@ -import "./store/clud/create-spec"; -import "./store/clud/destroy-spec"; -import "./store/clud/load-spec"; -import "./store/clud/update-spec"; -import "./store/core/add-spec"; -import "./store/core/define-spec"; -import "./store/core/find-spec"; -import "./store/core/push-spec"; -import "./store/core/remove-spec"; -import "./store/events/off-spec"; -import "./store/events/on-spec"; -import "./store/fields/attr-spec"; -import "./store/fields/has-many-spec"; -import "./store/fields/has-one-spec"; +import "./shared"; diff --git a/spec/shared.js b/spec/shared.js new file mode 100644 index 0000000..564c15c --- /dev/null +++ b/spec/shared.js @@ -0,0 +1,15 @@ +import "./store/clud/create-spec"; +import "./store/clud/destroy-spec"; +import "./store/clud/load-spec"; +import "./store/clud/update-spec"; +import "./store/core/add-spec"; +import "./store/core/convert-spec"; +import "./store/core/define-spec"; +import "./store/core/find-spec"; +import "./store/core/push-spec"; +import "./store/core/remove-spec"; +import "./store/events/off-spec"; +import "./store/events/on-spec"; +import "./store/fields/attr-spec"; +import "./store/fields/has-many-spec"; +import "./store/fields/has-one-spec"; diff --git a/spec/store/clud/create-spec.js b/spec/store/clud/create-spec.js index f9bf17a..a29101e 100644 --- a/spec/store/clud/create-spec.js +++ b/spec/store/clud/create-spec.js @@ -17,7 +17,7 @@ test("create must call the create method prodvided by the adapter", function (t) var cb = function () {}; t.plan(2); t.doesNotThrow(function () { - store.create("foo", a, cb); + store.create(a, cb); }, "should not throw an error"); - t.ok(adatper.create.calledWith(store, "foo", a, cb), "should call adapter with the same params"); + t.ok(adatper.create.calledWith(store, a, cb), "should call adapter with the same params"); }); diff --git a/spec/store/clud/destroy-spec.js b/spec/store/clud/destroy-spec.js index 44862d9..191bba5 100644 --- a/spec/store/clud/destroy-spec.js +++ b/spec/store/clud/destroy-spec.js @@ -13,10 +13,11 @@ 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 store = new Store(adatper); + var a = {}; var cb = function () {}; t.plan(2); t.doesNotThrow(function () { - store.destroy("foo", "1", cb); + store.destroy(a, cb); }, "should not throw an error"); - t.ok(adatper.destroy.calledWith(store, "foo", "1", cb), "should call adapter with the same params"); + t.ok(adatper.destroy.calledWith(store, a, cb), "should call adapter with the same params"); }); diff --git a/spec/store/clud/update-spec.js b/spec/store/clud/update-spec.js index 0e257e3..50c780a 100644 --- a/spec/store/clud/update-spec.js +++ b/spec/store/clud/update-spec.js @@ -17,7 +17,7 @@ test("update must call the update method prodvided by the adapter", function (t) var cb = function () {}; t.plan(2); t.doesNotThrow(function () { - store.update("foo", "1", a, cb); + store.update(a, cb); }, "should not throw an error"); - t.ok(adatper.update.calledWith(store, "foo", "1", a, cb), "should call adapter with the same params"); + t.ok(adatper.update.calledWith(store, a, cb), "should call adapter with the same params"); }); diff --git a/spec/store/core/convert-spec.js b/spec/store/core/convert-spec.js new file mode 100644 index 0000000..d26699b --- /dev/null +++ b/spec/store/core/convert-spec.js @@ -0,0 +1,25 @@ +import test from "tape-catch"; +import sinon from "sinon"; +import Store from "../../../src/store"; + +test("convert must use serialize functions provided by type definitions", function (t) { + var store = new Store(); + var serialize = sinon.spy(function (resource, data) { + data.attributes.title = resource.title + "!"; + }); + var resource = { + type: "products", + id: "44", + title: "Example" + }; + t.plan(3); + store.define("products", { + title: { + serialize: serialize + } + }); + store.convert(resource).attributes.title + t.equal(serialize.firstCall.args[0], resource); + t.deepEqual(serialize.firstCall.args[1], { type: "products", id: "44", attributes: { title: "Example!" }, relationships: {} }); + t.equal(serialize.firstCall.args[2], "title"); +}); diff --git a/spec/store/core/define-spec.js b/spec/store/core/define-spec.js index 67c6410..818c5a7 100644 --- a/spec/store/core/define-spec.js +++ b/spec/store/core/define-spec.js @@ -36,3 +36,11 @@ test("define must throw an error if you try to define a type that has already be store.define([ "sample", "example"], {}); }, /The type 'example' has already been defined\./); }); + +test("define must throw an error if you try to define a type that without providing a definition", function (t) { + var store = new Store(); + t.plan(1); + t.throws(function () { + store.define("example"); + }, /You must provide a definition for the type 'example'\./); +}); diff --git a/spec/store/fields/attr-spec.js b/spec/store/fields/attr-spec.js index 0939db2..5c05531 100644 --- a/spec/store/fields/attr-spec.js +++ b/spec/store/fields/attr-spec.js @@ -6,7 +6,7 @@ test("attr must return the correct type attribute", function (t) { t.equal(Store.attr().type, "attr"); }); -test("attr must return a deserialize function that passes on a default option", function (t) { +test("attr must return a definition that has the default option passed", function (t) { t.plan(2); t.equal(Store.attr({ default: "foo" }).default, "foo"); t.equal(Store.attr("example", { default: "foo" }).default, "foo"); @@ -39,6 +39,43 @@ test("attr must return a deserialize function that maps to the key if no attribu t.equal(field.deserialize.call(store, data, "title"), "Example"); }); +test("attr must return a serialize function that maps to the attribute provided", function (t) { + var field = Store.attr("example-title"); + var resource = { + "type": "products", + "id": "1", + "example-title": "Example" + }; + var data = { + type: "products", + id: "1", + attributes: {}, + relationships: {} + }; + t.plan(1); + field.serialize.call(this, resource, data, "example-title"); + t.equal(data.attributes["example-title"], "Example"); +}); + +test("attr must return a serialize function that maps to the key if no attribute name is provided", function (t) { + var store = new Store(); + var field = Store.attr(); + var resource = { + "type": "products", + "id": "1", + "title": "Example" + }; + var data = { + type: "products", + id: "1", + attributes: {}, + relationships: {} + }; + t.plan(1); + field.serialize.call(store, resource, data, "title"); + t.equal(data.attributes["title"], "Example"); +}); + test("attr must return undefined when the attribute is missing from the data", function (t) { var store = new Store(); var field = Store.attr("title"); diff --git a/src/ajax-adapter.js b/src/ajax-adapter.js index bfe48ac..c6ef49f 100644 --- a/src/ajax-adapter.js +++ b/src/ajax-adapter.js @@ -1,24 +1,37 @@ export default class AjaxAdapter { - // create(store, type, data, callback) { - // - // let request = new XMLHttpRequest(); - // - // request.open('POST', `/${type}`, true); - // - // request.onload = function () { - // var data = JSON.parse(request.responseText); - // store.push(data); - // callback(store.find(type, data.id)); - // }; - // - // // we need to convert to data here. - // - // request.send({ - // data: JSON.stringify(data) - // }); - // - // } + create(store, resource, callback) { + + let request = new XMLHttpRequest(); + + request.open('POST', `/${resource.type}`, true); + + request.onload = function () { + var response = JSON.parse(request.responseText); + store.push(response); + callback(store.find(response.data.type, response.data.id)); + }; + + request.send({ + data: JSON.stringify(store.convert(resource)) + }); + + } + + destroy(store, resource, callback) { + + let request = new XMLHttpRequest(); + + request.open('DELETE', `/${resource.type}/${resource.id}`, true); + + request.onload = function () { + store.remove(resource.type, resource.id); + callback(); + }; + + request.send(); + + } load(store, type, id, options, callback) { @@ -49,4 +62,22 @@ export default class AjaxAdapter { } + update(store, resource, callback) { + + let request = new XMLHttpRequest(); + let data = store.convert(resource); + + request.open('PATCH', `/${resource.type}/${resource.id}`, true); + + request.onload = function () { + store.add(data); + callback(store.find(data.type, data.id)); + }; + + request.send({ + data: JSON.stringify(data) + }); + + } + } diff --git a/src/store.js b/src/store.js index d744fc1..17c3dec 100644 --- a/src/store.js +++ b/src/store.js @@ -21,6 +21,9 @@ export default class Store { default: options && options.default, deserialize: function (data, key) { return data.attributes && data.attributes[name || key]; + }, + serialize: function (resource, data, key) { + data.attributes[name || key] = resource[key]; } }; } @@ -131,9 +134,25 @@ export default class Store { } } - create(type, data, callback) { + convert(resource) { + var data = { + type: resource.type, + id: resource.id, + attributes: {}, + relationships: {} + }; + var definition = this._types[data.type]; + Object.keys(definition).forEach(fieldName => { + if (fieldName[0] !== "_") { + definition[fieldName].serialize(resource, data, fieldName); + } + }); + return data; + } + + create(resource, callback) { if (this._adapter) { - this._adapter.create(this, type, data, callback); + this._adapter.create(this, resource, callback); } else { throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); } @@ -149,19 +168,23 @@ export default class Store { */ define(names, definition) { names = (names.constructor === Array) ? names : [ names ]; - definition._names = names; - names.forEach(name => { - if (!this._types[name]) { - this._types[name] = definition; - } else { - throw new Error(`The type '${name}' has already been defined.`); - } - }); + if (definition) { + definition._names = names; + names.forEach(name => { + if (!this._types[name]) { + this._types[name] = definition; + } else { + throw new Error(`The type '${name}' has already been defined.`); + } + }); + } else { + throw new Error(`You must provide a definition for the type '${names[0]}'.`); + } } - destroy(type, id, callback) { + destroy(resource, callback) { if (this._adapter) { - this._adapter.destroy(this, type, id, callback); + this._adapter.destroy(this, resource, callback); } else { throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); } @@ -354,9 +377,9 @@ export default class Store { } } - update(type, id, data, callback) { + update(resource, callback) { if (this._adapter) { - this._adapter.update(this, type, id, data, callback); + this._adapter.update(this, resource, callback); } else { throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); } -- 2.51.2 From 0b12330b63c05c725f2cfcda5eb33012be329a57 Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Fri, 4 Sep 2015 15:38:02 +0930 Subject: [PATCH 14/23] Minor change to the test script. --- CONTRIBUTING.md | 2 +- scripts/test | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fb1de34..af4b8b6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ npm test Alternatively, you can run tests in watch mode: ``` -npm test -- -w +npm test watch ``` ## Generating Documentation diff --git a/scripts/test b/scripts/test index 37953da..83ce893 100755 --- a/scripts/test +++ b/scripts/test @@ -10,7 +10,7 @@ if [ "$TARGET" = "node" ]; then exit $? fi -if [ "$1" = "--watch" ] || [ "$1" = "-w" ]; then +if [ "$1" = "watch" ]; then ./node_modules/.bin/chokidar './src/**/*.js' './spec/**/*.js' -c './scripts/test' else set -o pipefail -- 2.51.2 From 7251dda936cc7525840cf234e4fa907de4dc7b00 Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Fri, 4 Sep 2015 15:39:39 +0930 Subject: [PATCH 15/23] Flesh-out the create, load, update and destroy AJAX methods. --- spec/adapters/ajax/create-spec.js | 56 ++++++++++-- spec/adapters/ajax/destroy-spec.js | 56 +++++++++++- spec/adapters/ajax/load-spec.js | 75 ++++++++++++++-- spec/adapters/ajax/update-spec.js | 49 ++++++++++- spec/store/clud/create-spec.js | 11 ++- spec/store/clud/destroy-spec.js | 11 ++- spec/store/clud/load-spec.js | 11 ++- spec/store/clud/update-spec.js | 12 ++- spec/store/core/convert-spec.js | 33 +++++-- src/ajax-adapter.js | 70 ++++++++++----- src/store.js | 137 ++++++++++++++++++++++++----- 11 files changed, 442 insertions(+), 79 deletions(-) diff --git a/spec/adapters/ajax/create-spec.js b/spec/adapters/ajax/create-spec.js index ecf528f..a39b3f7 100644 --- a/spec/adapters/ajax/create-spec.js +++ b/spec/adapters/ajax/create-spec.js @@ -3,7 +3,7 @@ import sinon from "sinon"; import Store from "../../../src/store"; test("create must post a resource to the server and add it to the store on success", function (t) { - var server = sinon.fakeServer.create({ autoRespond: true }); + var server = sinon.fakeServer.create({ autoRespond: false }); var adapter = new Store.AjaxAdapter(); var store = new Store(adapter); t.plan(2); @@ -26,15 +26,61 @@ test("create must post a resource to the server and add it to the store on succe } }) ]); - store.create({ type: "products", title: "My Book" }, function (product) { + store.create("products", { title: "My Book" }, function (product) { t.equal(product.title, "My Book"); t.equal(store.find("products", "9").title, "My Book"); }); + server.respond(); server.restore(); }); -test.skip("must throw an error if resource is missing a 'type' property"); +test("create must handle 500 errors for failed attempts", function (t) { + var server = sinon.fakeServer.create({ autoRespond: false }); + var adapter = new Store.AjaxAdapter(); + var store = new Store(adapter); + t.plan(2); + t.timeoutAfter(1000); + store.define("products", {}); + server.respondWith("POST", "/products", [ + 500, + { "Content-Type": "application/vnd.api+json" }, + JSON.stringify({ + data: { type: "products", id: "9" } + }) + ]); + t.equal(store.find("products").length, 0); + store.create("products", {}, function () { + t.fail("must not call the success callback"); + }, function () { + t.equal(store.find("products").length, 0); + }); + server.respond(); + server.restore(); +}); -test.skip("must call the error callback if an undefined type is included"); +test.skip("create must call the error callback if an undefined type is included"); -test.skip("must call the error callback if the server responds with a non-2xx code"); +test.skip("create must call the error callback if the server responds with a non-2xx code"); + +test.skip("create must call callbacks with the context provided"); + +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" }); + var store = new Store(adapter); + t.plan(1); + t.timeoutAfter(1000); + store.define("products", {}); + server.respondWith("POST", "http://example.com/products", [ + 201, + { "Content-Type": "application/vnd.api+json" }, + JSON.stringify({ + data: { type: "products", id: "9" } + }) + ]); + store.create("products", {}, function (product) { + t.equal(store.find("products", "9"), product); + }); + server.respond(); + server.restore(); +}); diff --git a/spec/adapters/ajax/destroy-spec.js b/spec/adapters/ajax/destroy-spec.js index 4161ce1..3fe5f23 100644 --- a/spec/adapters/ajax/destroy-spec.js +++ b/spec/adapters/ajax/destroy-spec.js @@ -3,7 +3,7 @@ import sinon from "sinon"; import Store from "../../../src/store"; test("destroy must delete a resource from the server and remove it from the store on success", function (t) { - var server = sinon.fakeServer.create({ autoRespond: true }); + var server = sinon.fakeServer.create({ autoRespond: false }); var adapter = new Store.AjaxAdapter(); var store = new Store(adapter); t.plan(2); @@ -21,8 +21,60 @@ 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(store.find("products", "6"), function () { + store.destroy("products", "6", function () { t.equal(store.find("products").length, 0); }); + server.respond(); server.restore(); }); + +test("destroy must handle 500 errors for failed attempts", function (t) { + var server = sinon.fakeServer.create({ autoRespond: false }); + var adapter = new Store.AjaxAdapter(); + var store = new Store(adapter); + t.plan(2); + t.timeoutAfter(1000); + store.define("products", {}); + server.respondWith("DELETE", "/products/6", [ + 500, + { + "Content-Type": "application/vnd.api+json" + }, + "" + ]); + store.add({ + type: "products", + id: "6" + }); + t.equal(store.find("products").length, 1); + store.destroy("products", "6", function () { + t.fail("must not call the success callback"); + }, function () { + t.equal(store.find("products").length, 1); + }); + server.respond(); + server.restore(); +}); + +test("destroy 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" }); + var store = new Store(adapter); + t.plan(2); + t.timeoutAfter(1000); + store.define("products", {}); + server.respondWith("DELETE", "http://example.com/products/2", [ + 204, + { "Content-Type": "application/vnd.api+json" }, + "" + ]); + store.add({ type: "products", id: "2" }); + t.equal(store.find("products").length, 1); + store.destroy("products", "2", function () { + t.equal(store.find("products").length, 0); + }); + server.respond(); + server.restore(); +}); + +test.skip("destroy must call callbacks with the context provided"); diff --git a/spec/adapters/ajax/load-spec.js b/spec/adapters/ajax/load-spec.js index 169c2d0..92b6043 100644 --- a/spec/adapters/ajax/load-spec.js +++ b/spec/adapters/ajax/load-spec.js @@ -3,7 +3,7 @@ import sinon from "sinon"; import Store from "../../../src/store"; test("load must fetch a single resource from the server and add it to the store", function (t) { - var server = sinon.fakeServer.create({ autoRespond: true }); + var server = sinon.fakeServer.create({ autoRespond: false }); var adapter = new Store.AjaxAdapter(); var store = new Store(adapter); t.plan(5); @@ -57,11 +57,12 @@ test("load must fetch a single resource from the server and add it to the store" t.deepEqual(store.find("products", "12").comments.map(c => c.id).sort(), [ "2", "4" ]); t.deepEqual(store.find("products", "12").comments.map(c => c.type).sort(), [ "comments", "comments" ]); }); + server.respond(); server.restore(); }); test("load must fetch a collection of resources from the server and add them to the store", function (t) { - var server = sinon.fakeServer.create({ autoRespond: true }); + var server = sinon.fakeServer.create({ autoRespond: false }); var adapter = new Store.AjaxAdapter(); var store = new Store(adapter); t.plan(3); @@ -107,11 +108,75 @@ test("load must fetch a collection of resources from the server and add them to }, function (error) { t.fail(error); }); + server.respond(); server.restore(); }); -test.skip("must call the error callback if an undefined type is included"); +test("load must handle 500 errors for failed attempts", function (t) { + var server = sinon.fakeServer.create({ autoRespond: false }); + var adapter = new Store.AjaxAdapter(); + var store = new Store(adapter); + t.plan(2); + t.timeoutAfter(1000); + store.define("products", {}); + server.respondWith("GET", "/products/12", [ + 500, + { "Content-Type": "application/vnd.api+json" }, + "" + ]); + t.equal(store.find("products").length, 0); + store.load("products", "12", function () { + t.fail("must not call the success callback"); + }, function () { + t.equal(store.find("products").length, 0); + }); + server.respond(); + server.restore(); +}); + +test("load 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" }); + var store = new Store(adapter); + t.plan(3); + t.timeoutAfter(1000); + store.define("products", {}); + server.respondWith("GET", "http://example.com/products/9", [ + 200, + { "Content-Type": "application/vnd.api+json" }, + JSON.stringify({ + data: { type: "products", id: "9" } + }) + ]); + server.respondWith("GET", "http://example.com/products", [ + 200, + { "Content-Type": "application/vnd.api+json" }, + JSON.stringify({ + data: [ + { type: "products", id: "2" }, + { type: "products", id: "4" }, + { type: "products", id: "7" } + ] + }) + ]); + t.equal(store.find("products").length, 0); + store.load("products", "9", function () { + t.equal(store.find("products").length, 1); + store.load("products", function () { + t.deepEqual(store.find("products").map(x => x.id).sort(), [ "2", "4", "7", "9" ]); + }); + server.respond(); + }); + server.respond(); + server.restore(); +}); + +test.skip("load must use pseudonyms"); // store.load('product') OR store.load('products') + +test.skip("load must call the error callback if an undefined type is included"); + +test.skip("load must return a promise if no callbacks are provided"); -test.skip("must call the error callback if the server responds with a non-2xx code"); +test.skip("load must call callbacks with the context provided"); -test.skip("must return a promise if no callbacks are provided"); +test.skip("load must use the options if they're provided"); diff --git a/spec/adapters/ajax/update-spec.js b/spec/adapters/ajax/update-spec.js index 6311759..7adb490 100644 --- a/spec/adapters/ajax/update-spec.js +++ b/spec/adapters/ajax/update-spec.js @@ -3,7 +3,7 @@ import sinon from "sinon"; import Store from "../../../src/store"; test("update must update a resource on the server and add reflect the changes in the store on success", function (t) { - var server = sinon.fakeServer.create({ autoRespond: true }); + var server = sinon.fakeServer.create({ autoRespond: false }); var adapter = new Store.AjaxAdapter(); var store = new Store(adapter); t.plan(2); @@ -25,9 +25,54 @@ test("update must update a resource on the server and add reflect the changes in title: "My Book" } }); - store.update({ type: "products", id: "9", title: "My Book!" }, function (product) { + store.update("products", "9", { title: "My Book!" }, function (product) { t.equal(product.title, "My Book!"); t.equal(store.find("products", "9").title, "My Book!"); }); + server.respond(); server.restore(); }); + +test("update must handle 500 errors for failed attempts", function (t) { + var server = sinon.fakeServer.create({ autoRespond: false }); + var adapter = new Store.AjaxAdapter(); + var store = new Store(adapter); + t.plan(2); + t.timeoutAfter(1000); + store.define("products", {}); + server.respondWith("PATCH", "/products/12", [ + 500, + { "Content-Type": "application/vnd.api+json" }, + "" + ]); + t.equal(store.find("products").length, 0); + store.update("products", "12", {}, function () { + t.fail("must not call the success callback"); + }, function () { + t.equal(store.find("products").length, 0); + }); + server.respond(); + server.restore(); +}); + +test("update 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" }); + var store = new Store(adapter); + t.plan(1); + t.timeoutAfter(1000); + store.define("products", {}); + server.respondWith("PATCH", "http://example.com/products/9", [ + 204, + { "Content-Type": "application/vnd.api+json" }, + "" + ]); + store.add({ type: "products", id: "9" }); + store.update("products", "9", {}, function () { + t.pass("should call success callback"); + }); + server.respond(); + server.restore(); +}); + +test.skip("update must call callbacks with the context provided"); diff --git a/spec/store/clud/create-spec.js b/spec/store/clud/create-spec.js index a29101e..69294ca 100644 --- a/spec/store/clud/create-spec.js +++ b/spec/store/clud/create-spec.js @@ -13,11 +13,14 @@ 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 store = new Store(adatper); - var a = {}; - var cb = function () {}; + var type = "foo"; + var partial = {}; + var success = function () {}; + var error = function () {}; + var context = {}; t.plan(2); t.doesNotThrow(function () { - store.create(a, cb); + store.create(type, partial, success, error, context); }, "should not throw an error"); - t.ok(adatper.create.calledWith(store, a, cb), "should call adapter with the same params"); + t.ok(adatper.create.calledWith(store, type, partial, success, error, context), "should call adapter with the same params"); }); diff --git a/spec/store/clud/destroy-spec.js b/spec/store/clud/destroy-spec.js index 191bba5..32dbb02 100644 --- a/spec/store/clud/destroy-spec.js +++ b/spec/store/clud/destroy-spec.js @@ -13,11 +13,14 @@ 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 store = new Store(adatper); - var a = {}; - var cb = function () {}; + var type = "foo"; + var id = "1"; + var success = function () {}; + var error = function () {}; + var context = {}; t.plan(2); t.doesNotThrow(function () { - store.destroy(a, cb); + store.destroy(type, id, success, error, context); }, "should not throw an error"); - t.ok(adatper.destroy.calledWith(store, a, cb), "should call adapter with the same params"); + t.ok(adatper.destroy.calledWith(store, type, id, success, error, context), "should call adapter with the same params"); }); diff --git a/spec/store/clud/load-spec.js b/spec/store/clud/load-spec.js index e6a41a7..0ececdd 100644 --- a/spec/store/clud/load-spec.js +++ b/spec/store/clud/load-spec.js @@ -13,10 +13,15 @@ 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 store = new Store(adatper); - var cb = function () {}; + var type = "foo"; + var id = "1"; + var options = {}; + var success = function () {}; + var error = function () {}; + var context = {}; t.plan(2); t.doesNotThrow(function () { - store.load("foo", "1", cb); + store.load(type, id, options, success, error, context); }, "should not throw an error"); - t.ok(adatper.load.calledWith(store, "foo", "1", cb), "should call adapter with the same params"); + t.ok(adatper.load.calledWith(store, type, id, options, success, error, context), "should call adapter with the same params"); }); diff --git a/spec/store/clud/update-spec.js b/spec/store/clud/update-spec.js index 50c780a..40f676c 100644 --- a/spec/store/clud/update-spec.js +++ b/spec/store/clud/update-spec.js @@ -13,11 +13,15 @@ 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 store = new Store(adatper); - var a = {}; - var cb = function () {}; + var type = "foo"; + var id = "1"; + var partial = {}; + var success = function () {}; + var error = function () {}; + var context = {}; t.plan(2); t.doesNotThrow(function () { - store.update(a, cb); + store.update(type, id, partial, success, error, context); }, "should not throw an error"); - t.ok(adatper.update.calledWith(store, a, cb), "should call adapter with the same params"); + t.ok(adatper.update.calledWith(store, type, id, partial, success, error, context), "should call adapter with the same params"); }); diff --git a/spec/store/core/convert-spec.js b/spec/store/core/convert-spec.js index d26699b..3d9dbb6 100644 --- a/spec/store/core/convert-spec.js +++ b/spec/store/core/convert-spec.js @@ -7,9 +7,7 @@ test("convert must use serialize functions provided by type definitions", functi var serialize = sinon.spy(function (resource, data) { data.attributes.title = resource.title + "!"; }); - var resource = { - type: "products", - id: "44", + var partial = { title: "Example" }; t.plan(3); @@ -18,8 +16,31 @@ test("convert must use serialize functions provided by type definitions", functi serialize: serialize } }); - store.convert(resource).attributes.title - t.equal(serialize.firstCall.args[0], resource); - t.deepEqual(serialize.firstCall.args[1], { type: "products", id: "44", attributes: { title: "Example!" }, relationships: {} }); + t.deepEqual(store.convert("products", "44", partial), { type: "products", id: "44", attributes: { title: "Example!" }, relationships: {} }); + t.equal(serialize.firstCall.args[0], partial); t.equal(serialize.firstCall.args[2], "title"); }); + +test("convert must automatically extract the type and id if they're not passed", function (t) { + var store = new Store(); + var partial = { + type: "products", + id: "44" + }; + t.plan(3); + store.define("products", {}); + t.doesNotThrow(function () { + store.convert(partial); + }); + t.equal(store.convert(partial).type, "products"); + t.equal(store.convert(partial).id, "44"); +}); + +test("convert must not include the id if it wasn't provided", function (t) { + var store = new Store(); + t.plan(3); + store.define("products", {}); + t.equal(store.convert({ type: "products" }).id, undefined); + t.equal(store.convert("products", {}).id, undefined); + t.equal(store.convert("products", null, {}).id, undefined); +}); diff --git a/src/ajax-adapter.js b/src/ajax-adapter.js index c6ef49f..5528a45 100644 --- a/src/ajax-adapter.js +++ b/src/ajax-adapter.js @@ -1,59 +1,79 @@ export default class AjaxAdapter { - create(store, resource, callback) { + constructor(options) { + this._base = (options && options.base) || ""; + }; + + create(store, type, partial, success, error, context) { let request = new XMLHttpRequest(); - request.open('POST', `/${resource.type}`, true); + request.open('POST', `${this._base}/${type}`, true); request.onload = function () { - var response = JSON.parse(request.responseText); - store.push(response); - callback(store.find(response.data.type, response.data.id)); + if (request.status >= 200 && request.status < 300) { + let response = JSON.parse(request.responseText); + store.push(response); + success.call(context, store.find(response.data.type, response.data.id)); + } else { + error.call(context); + } }; request.send({ - data: JSON.stringify(store.convert(resource)) + data: JSON.stringify(store.convert(type, partial)) }); } - destroy(store, resource, callback) { + destroy(store, type, id, success, error, context) { let request = new XMLHttpRequest(); - request.open('DELETE', `/${resource.type}/${resource.id}`, true); + request.open('DELETE', `${this._base}/${type}/${id}`, true); request.onload = function () { - store.remove(resource.type, resource.id); - callback(); + if (request.status >= 200 && request.status < 300) { + store.remove(type, id); + success.call(context); + } else { + error.call(context); + } }; request.send(); } - load(store, type, id, options, callback) { + load(store, type, id, options, success, error, context) { if (id && {}.toString.call(id) === '[object Function]') { - this.load(store, type, null, null, id); - } else if (options && {}.toString.call(options) === '[object Function]') { - this.load(store, type, id, null, options); + this.load(store, type, null, null, id, options, success); } else if (id && typeof id === "object") { - this.load(store, type, null, id, callback); + this.load(store, type, null, id, options, success, error); + } else if (options && {}.toString.call(options) === '[object Function]') { + this.load(store, type, id, null, options, success, error); } else { let request = new XMLHttpRequest(); let url; options = options || {}; - url = id ? `/${type}/${id}` : `/${type}`; + url = id ? `${this._base}/${type}/${id}` : `${this._base}/${type}`; request.open('GET', url, true); request.onload = function () { - store.push(JSON.parse(request.responseText)); - callback(store.find(type, id)); + if (request.status >= 200 && request.status < 300) { + store.push(JSON.parse(request.responseText)); + if (id) { + success.call(context, store.find(type, id)); + } else { + success.call(context, store.find(type)); + } + } else { + error.call(context); + } }; request.send(); @@ -62,16 +82,20 @@ export default class AjaxAdapter { } - update(store, resource, callback) { + update(store, type, id, partial, success, error, context) { let request = new XMLHttpRequest(); - let data = store.convert(resource); + let data = store.convert(type, id, partial); - request.open('PATCH', `/${resource.type}/${resource.id}`, true); + request.open('PATCH', `${this._base}/${type}/${id}`, true); request.onload = function () { - store.add(data); - callback(store.find(data.type, data.id)); + if (request.status >= 200 && request.status < 300) { + store.add(data); + success.call(context, store.find(data.type, data.id)); + } else { + error.call(context); + } }; request.send({ diff --git a/src/store.js b/src/store.js index 17c3dec..af61ad1 100644 --- a/src/store.js +++ b/src/store.js @@ -134,25 +134,61 @@ export default class Store { } } - convert(resource) { - var data = { - type: resource.type, - id: resource.id, - attributes: {}, - relationships: {} - }; - var definition = this._types[data.type]; - Object.keys(definition).forEach(fieldName => { - if (fieldName[0] !== "_") { - definition[fieldName].serialize(resource, data, fieldName); + /** + * Converts the given partial into a JSON API compliant representation. + * + * @since 0.5.0 + * @param {!string} [type] - The type of the resource. This can be omitted if the partial includes a type property. + * @param {!string} [id] - The id of the resource. This can be omitted if the partial includes an id property. + * @param {!object} partial - The data to convert. + * @return {object} - JSON API version of the object. + */ + convert(type, id, partial) { + if (type && typeof type === "object") { + return this.convert(type.type, type.id, type); + } else if (id && typeof id === "object") { + return this.convert(type, id.id, id); + } else { + let data = { + type: type, + attributes: {}, + relationships: {} + }; + if (id) { + data.id = id; } - }); - return data; + let definition = this._types[data.type]; + Object.keys(definition).forEach(fieldName => { + if (fieldName[0] !== "_") { + definition[fieldName].serialize(partial, data, fieldName); + } + }); + return data; + } } - create(resource, callback) { + /** + * Attempts to create the resource through the adapter and adds it to the + * store if successful. + * + * @since 0.5.0 + * @param {!string} type - Type of resource. + * @param {!Object} partial - Data to create the resource with. + * @param {function} [success] - Callback on success. + * @param {function} [error] - Callback on error. + * @param {Object} [context] - Context for the callbacks. + * @return {undefined} - Nothing. + * + * @example + * let adapter = new Store.AjaxAdapter(); + * let store = new Store(adpater); + * store.create("product", { title: "A Book" }, (product) => { + * console.log(product.title); + * }); + */ + create(type, partial, success, error, context) { if (this._adapter) { - this._adapter.create(this, resource, callback); + this._adapter.create(this, type, partial, success, error, context); } else { throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); } @@ -182,9 +218,28 @@ export default class Store { } } - destroy(resource, callback) { + /** + * Attempts to delete the resource through the adapter and removes it from + * the store if successful. + * + * @since 0.5.0 + * @param {!string} type - Type of resource. + * @param {!string} id - ID of resource. + * @param {function} [success] - Callback on success. + * @param {function} [error] - Callback on error. + * @param {Object} [context] - Context for the callbacks. + * @return {undefined} - Nothing. + * + * @example + * let adapter = new Store.AjaxAdapter(); + * let store = new Store(adpater); + * store.destroy("product", "1", () => { + * console.log("Destroyed!"); + * }); + */ + destroy(type, id, success, error, context) { if (this._adapter) { - this._adapter.destroy(this, resource, callback); + this._adapter.destroy(this, type, id, success, error, context); } else { throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); } @@ -235,9 +290,29 @@ export default class Store { } } - load(type, id, callback) { + /** + * Attempts to load the resource(s) through the adapter and adds it/them to + * the store if successful. + * + * @since 0.5.0 + * @param {!string} type - Type of resource. + * @param {!string} [id] - ID of resource. + * @param {Object} [options] - **NOT YET IMPLEMENTED** (this will include sorting, filtering and pagination options) + * @param {function} [success] - Callback on success. + * @param {function} [error] - Callback on error. + * @param {Object} [context] - Context for the callbacks. + * @return {undefined} - Nothing. + * + * @example + * let adapter = new Store.AjaxAdapter(); + * let store = new Store(adpater); + * store.load("product", "1", (product) => { + * console.log(product.title); + * }); + */ + load(type, id, options, success, error, context) { if (this._adapter) { - this._adapter.load(this, type, id, callback); + this._adapter.load(this, type, id, options, success, error, context); } else { throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); } @@ -377,9 +452,29 @@ export default class Store { } } - update(resource, callback) { + /** + * Attempts to update the resource through the adapter and updates it in the + * store if successful. + * + * @since 0.5.0 + * @param {!string} type - Type of resource. + * @param {!string} id - ID of resource. + * @param {!Object} partial - Data to update the resource with. + * @param {function} [success] - Callback on success. + * @param {function} [error] - Callback on error. + * @param {Object} [context] - Context for the callbacks. + * @return {undefined} - Nothing. + * + * @example + * let adapter = new Store.AjaxAdapter(); + * let store = new Store(adpater); + * store.update("product", "1", { title: "foo" }, (product) => { + * console.log(product.title); + * }); + */ + update(type, id, partial, success, error, context) { if (this._adapter) { - this._adapter.update(this, resource, callback); + this._adapter.update(this, type, id, partial, success, error, context); } else { throw new Error("Adapter missing. Specify an adapter when creating the store: `var store = new Store(adapter);`"); } -- 2.51.2 From 38a9be5d0751b0b359030c685de78da9d1e42fcc Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Tue, 8 Sep 2015 11:09:51 +0930 Subject: [PATCH 16/23] Add ESLint dev dependency. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 89851e5..bbdac40 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "browserify": "^11.0.1", "chokidar-cli": "^1.0.1", "esdoc": "^0.2.2", + "eslint": "^1.3.1", "faucet": "0.0.1", "sinon": "^1.16.1", "tape": "^4.2.0", -- 2.51.2 From ebda5e2a42950cd9b9ea5f59b11153bb2006166d Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Tue, 8 Sep 2015 11:34:04 +0930 Subject: [PATCH 17/23] Update the README. --- README.md | 106 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 70 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 629bb0c..47c4265 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ # JSON API Store [![Build Status](https://travis-ci.org/haydn/json-api-store.svg?branch=master)](https://travis-ci.org/haydn/json-api-store) -JSON API Store takes [JSON API](http://jsonapi.org) data and creates plain -JavaScript objects for the resources it describes. It will link-up -relationships automatically and update both sides of reciprocal relationships -when either side is modified. +A lightweight JavaScript library that acts as a data store and uses the +[JSON API](http://jsonapi.org) specification. You can use it in the browser to +easily communicate with a JSON API compliant server. ## Usage -At the moment you need to do your own AJAX requests, but there are plans to add -AJAX methods to create, read, update and destroy resources in the future. For -now you can just push the responses from your own requests to the store: +### Browser + +At the moment the primary use can for JSON API Store is in the browser: ```javascript // Create a new store instance. -var store = new Store(); +var adapter = new Store.AjaxAdapter({ base: "http://example.com/api/v1" }); +var store = new Store(adapter); // Define the "categories" type. store.define([ "categories", "category" ], { @@ -28,39 +28,71 @@ store.define([ "products", "product" ], { category: Store.hasOne() }); -// Add data - this can just be the response from a GET request to your API. -store.push({ - "data": { - "type": "products", - "id": "1", - "attributes": { - "title": "Example Book" - }, - "relationships": { - "category": { - "data": { - "type": "categories", - "id": "1" - } - } - } +// Load all the products. +store.load("products", { include: [ "category" ] }, function (products) { + + products.length; // 1 + products[0].id; // "1" + products[0].title; // "Example Book" + products[0].category.id; // "1" + products[0].category.title; // "Books" + + products[0] === store.find("products", "1"); // true + products[0].category === store.find("categories", "1"); // true + +}); + +``` + +### Node + +You can also use JSON API Store in a Node.js environment (adapters that work in +a Node.js are in the works): + +**NOTE**: Without an adapter the `create`, `load`, `update` and `destroy` +methods cannot be used. + +```javascript + +var Store = require("json-api-store"); + +var store = new Store(); + +store.define([ "categories", "category" ], { + title: Store.attr(), + products: Store.hasMany() +}); + +store.define([ "products", "product" ], { + title: Store.attr(), + category: Store.hasOne() +}); + +store.add({ + type: "products", + id: "1", + attributes: { + title: "Example Product" }, - "included": [ - { - "type": "categories", - "id": "1", - "attributes": { - "title": "Books" + relationships: { + category: { + data: { + type: "categories", + id: "1" } } - ] + } }); -store.find("products", "1").title; // "Example Book" -store.find("categories", "1").title; // "Books" +store.add({ + type: "categories", + id: "1", + attributes: { + title: "Example Category" + } +}); -store.find("products", "1").category === store.find("categories", "1"); // true -store.find("categories", "1").products[0] === store.find("products", "1"); // true +store.find("products", "1").category.title; // "Example Category" ``` @@ -84,4 +116,6 @@ Grab the [store.js](https://raw.githubusercontent.com/haydn/json-api-store/maste ## Documentation -Documentation is available in the `docs` directory. Online documentation is coming soon. +Documentation is available on the website: + +http://particlesystem.com/json-api-store/ -- 2.51.2 From 3019dfbe628b3cd646dbce89417e1e5f4c7da81a Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Tue, 22 Sep 2015 11:31:43 +0930 Subject: [PATCH 18/23] Improve test script. --- scripts/release | 4 ++-- scripts/test | 41 ++++++++++++++++++++++++++++------------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/scripts/release b/scripts/release index 312e685..4bc961e 100755 --- a/scripts/release +++ b/scripts/release @@ -16,8 +16,8 @@ git pull echo "$ npm install" npm install -echo "$ npm test" -npm test +echo "$ npm test node && npm test browser" +npm test node && npm test browser if [ $? -ne 0 ]; then echo >&2 "Looks like there are failing tests. Make sure all tests pass and run this script again." diff --git a/scripts/test b/scripts/test index 83ce893..147aab9 100755 --- a/scripts/test +++ b/scripts/test @@ -1,25 +1,40 @@ #!/usr/bin/env bash -if [ "$TARGET" = "browser" ]; then +## Travis CI + +if [ "$TARGET" = "browser" ] +then ./node_modules/.bin/browserify spec/client.js -t babelify | node_modules/.bin/tape-run exit $? -fi - -if [ "$TARGET" = "node" ]; then +elif [ "$TARGET" = "node" ] +then ./node_modules/.bin/babel-node spec/server.js exit $? fi -if [ "$1" = "watch" ]; then - ./node_modules/.bin/chokidar './src/**/*.js' './spec/**/*.js' -c './scripts/test' +## Development + +if [ "$2" = "watch" ] +then + ./node_modules/.bin/chokidar './src/**/*.js' './spec/**/*.js' -c "./scripts/test $1" + exit 0 else - set -o pipefail - echo "NODE:" - ./node_modules/.bin/babel-node spec/server.js | node_modules/.bin/faucet - if [ $? -eq 0 ]; then - echo "BROWSER:" + if [ "$1" = "browser" ] + then + set -o pipefail + echo "== TESTING IN BROWSER ==" ./node_modules/.bin/browserify spec/client.js -d -t babelify | node_modules/.bin/tape-run | node_modules/.bin/faucet + exit $? + elif [ "$1" = "node" ] + then + set -o pipefail + echo "== TESTING IN NODE ==" + ./node_modules/.bin/babel-node spec/server.js | node_modules/.bin/faucet + exit $? + else + echo "Target not specified. Usage examples:" + echo " npm test node" + echo " npm test browser watch" + exit 0 fi fi - -exit $? -- 2.51.2 From 6e982844b85c4e26283e2fa767a1c49ff08725c2 Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Tue, 22 Sep 2015 11:33:18 +0930 Subject: [PATCH 19/23] Fix bug where remove throws an error if a resource doesn't exist. --- spec/store/core/remove-spec.js | 9 +++++++++ src/store.js | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/spec/store/core/remove-spec.js b/spec/store/core/remove-spec.js index f4b0c6f..dda2a15 100644 --- a/spec/store/core/remove-spec.js +++ b/spec/store/core/remove-spec.js @@ -51,6 +51,15 @@ test("remove must throw an error if the type has not been defined", function (t) }, /Unknown type 'products'/); }); +test("remove must not throw an error if the a resource doesn't exist", function (t) { + var store = new Store(); + t.plan(1); + store.define("products", {}); + t.doesNotThrow(function () { + store.remove("products", "1"); + }); +}); + test("remove must remove dependant relationships when a resource is removed", function (t) { var store = new Store(); t.plan(3); diff --git a/src/store.js b/src/store.js index af61ad1..3846a57 100644 --- a/src/store.js +++ b/src/store.js @@ -431,7 +431,7 @@ export default class Store { if (type) { if (this._types[type]) { if (id) { - let resource = this._data[type][id]; + let resource = this._data[type] && this._data[type][id]; if (resource) { this._remove(resource); if (this._resourceListeners["removed"][type] && this._resourceListeners["removed"][type][id]) { -- 2.51.2 From 84528ce814e2ff25fa8291a1bcc6981c4421c6aa Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Tue, 22 Sep 2015 11:34:02 +0930 Subject: [PATCH 20/23] Improve CLUD methods in the AJAX Adatper. --- spec/adapters/ajax/create-spec.js | 111 +++++++++++++++++++++++++-- spec/adapters/ajax/destroy-spec.js | 86 ++++++++++++++++++++- spec/adapters/ajax/load-spec.js | 98 ++++++++++++++++++++++-- spec/adapters/ajax/update-spec.js | 25 +++++- src/ajax-adapter.js | 118 ++++++++++++++++++++--------- 5 files changed, 388 insertions(+), 50 deletions(-) diff --git a/spec/adapters/ajax/create-spec.js b/spec/adapters/ajax/create-spec.js index a39b3f7..8f1a34b 100644 --- a/spec/adapters/ajax/create-spec.js +++ b/spec/adapters/ajax/create-spec.js @@ -44,9 +44,7 @@ test("create must handle 500 errors for failed attempts", function (t) { server.respondWith("POST", "/products", [ 500, { "Content-Type": "application/vnd.api+json" }, - JSON.stringify({ - data: { type: "products", id: "9" } - }) + "" ]); t.equal(store.find("products").length, 0); store.create("products", {}, function () { @@ -58,11 +56,110 @@ test("create must handle 500 errors for failed attempts", function (t) { server.restore(); }); -test.skip("create must call the error callback if an undefined type is included"); +test("create must call the error callback if an error is raised during the process", function (t) { + var server = sinon.fakeServer.create({ autoRespond: false }); + var adapter = new Store.AjaxAdapter(); + var store = new Store(adapter); + var callback = sinon.spy(); + t.plan(1); + t.timeoutAfter(1000); + store.define("products", {}); + server.respondWith("POST", "/products", [ + 201, + { + "Content-Type": "application/vnd.api+json" + }, + JSON.stringify({ + data: { + type: "products", + id: "9" + }, + included: [ + { + type: "foo", + id: "1" + } + ] + }) + ]); + store.create("products", {}, null, callback); + server.respond(); + t.equal(callback.callCount, 1); + server.restore(); +}); -test.skip("create must call the error callback if the server responds with a non-2xx code"); +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.skip("create must call callbacks with the context provided"); +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 }); @@ -84,3 +181,5 @@ test("create must use the adapter's 'base' config if present", function (t) { server.respond(); server.restore(); }); + +test.skip("create must throw an error if the type has not been defined", function (t) {}); diff --git a/spec/adapters/ajax/destroy-spec.js b/spec/adapters/ajax/destroy-spec.js index 3fe5f23..36e6551 100644 --- a/spec/adapters/ajax/destroy-spec.js +++ b/spec/adapters/ajax/destroy-spec.js @@ -77,4 +77,88 @@ test("destroy must use the adapter's 'base' config if present", function (t) { server.restore(); }); -test.skip("destroy must call callbacks with the context provided"); +test("destroy must call the error callback if an error is raised during the process", function (t) { + var server = sinon.fakeServer.create({ autoRespond: false }); + var adapter = new Store.AjaxAdapter(); + var store = new Store(adapter); + var callback = sinon.spy(); + t.plan(1); + t.timeoutAfter(1000); + server.respondWith("DELETE", "/foo/1", [ + 204, + { "Content-Type": "application/vnd.api+json" }, + "" + ]); + store.destroy("foo", "1", null, callback); + server.respond(); + t.equal(callback.callCount, 1); + 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.skip("destroy must throw an error if the type has not been defined", function (t) {}); diff --git a/spec/adapters/ajax/load-spec.js b/spec/adapters/ajax/load-spec.js index 92b6043..f964b11 100644 --- a/spec/adapters/ajax/load-spec.js +++ b/spec/adapters/ajax/load-spec.js @@ -171,12 +171,100 @@ test("load must use the adapter's 'base' config if present", function (t) { server.restore(); }); -test.skip("load must use pseudonyms"); // store.load('product') OR store.load('products') - -test.skip("load must call the error callback if an undefined type is included"); +test("load must call the error callback if an error is raised during the process", function (t) { + var server = sinon.fakeServer.create({ autoRespond: false }); + var adapter = new Store.AjaxAdapter(); + var store = new Store(adapter); + var callback = sinon.spy(); + t.plan(1); + t.timeoutAfter(1000); + store.define("products", {}); + server.respondWith("GET", "/products/1", [ + 200, + { "Content-Type": "application/vnd.api+json" }, + JSON.stringify({ + data: { type: "products", id: "1" }, + included: [ + { type: "foo", id: "1" }, + ] + }) + ]); + store.load("products", "1", function () {}, callback); + server.respond(); + t.equal(callback.callCount, 1); + server.restore(); +}); -test.skip("load must return a promise if no callbacks are provided"); +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.skip("load must call callbacks with the context provided"); +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.skip("load must use the options if they're provided"); + +test.skip("load must throw an error if the type has not been defined", function (t) {}); diff --git a/spec/adapters/ajax/update-spec.js b/spec/adapters/ajax/update-spec.js index 7adb490..f19f88a 100644 --- a/spec/adapters/ajax/update-spec.js +++ b/spec/adapters/ajax/update-spec.js @@ -75,4 +75,27 @@ test("update must use the adapter's 'base' config if present", function (t) { server.restore(); }); -test.skip("update must call callbacks with the context provided"); +test("update must call the error callback if an error is raised during the process", function (t) { + var server = sinon.fakeServer.create({ autoRespond: false }); + var adapter = new Store.AjaxAdapter(); + var store = new Store(adapter); + 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.skip("update must handle missing success or error callbacks", function (t) {}); + +test.skip("update must call callbacks with the context provided", function (t) {}); + +test.skip("update must throw an error if the type has not been defined", function (t) {}); diff --git a/src/ajax-adapter.js b/src/ajax-adapter.js index 5528a45..5a79d07 100644 --- a/src/ajax-adapter.js +++ b/src/ajax-adapter.js @@ -6,42 +6,74 @@ export default class AjaxAdapter { create(store, type, partial, success, error, context) { - let request = new XMLHttpRequest(); + if (error && {}.toString.call(error) !== '[object Function]') { - request.open('POST', `${this._base}/${type}`, true); + this.create(store, type, partial, success, null, error); - request.onload = function () { - if (request.status >= 200 && request.status < 300) { - let response = JSON.parse(request.responseText); - store.push(response); - success.call(context, store.find(response.data.type, response.data.id)); - } else { - error.call(context); - } - }; + } else { - request.send({ - data: JSON.stringify(store.convert(type, partial)) - }); + let request = new XMLHttpRequest(); + + request.open('POST', `${this._base}/${type}`, true); + + 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({ + data: JSON.stringify(store.convert(type, partial)) + }); + + } } destroy(store, type, id, success, error, context) { - let request = new XMLHttpRequest(); + if (error && {}.toString.call(error) !== '[object Function]') { - request.open('DELETE', `${this._base}/${type}/${id}`, true); + this.destroy(store, type, id, success, null, error); - request.onload = function () { - if (request.status >= 200 && request.status < 300) { - store.remove(type, id); - success.call(context); - } else { - error.call(context); - } - }; + } else { + + let request = new XMLHttpRequest(); + + request.open('DELETE', `${this._base}/${type}/${id}`, true); + + 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(); + request.send(); + + } } @@ -52,7 +84,9 @@ export default class AjaxAdapter { } 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, null, options, success, error); + 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 { let request = new XMLHttpRequest(); @@ -65,13 +99,15 @@ export default class AjaxAdapter { request.onload = function () { if (request.status >= 200 && request.status < 300) { - store.push(JSON.parse(request.responseText)); - if (id) { - success.call(context, store.find(type, id)); - } else { - success.call(context, store.find(type)); + 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 { + } else if (error) { error.call(context); } }; @@ -82,7 +118,7 @@ export default class AjaxAdapter { } - update(store, type, id, partial, success, error, context) { + update(store, type, id, partial, success, error) { let request = new XMLHttpRequest(); let data = store.convert(type, id, partial); @@ -91,10 +127,18 @@ export default class AjaxAdapter { request.onload = function () { if (request.status >= 200 && request.status < 300) { - store.add(data); - success.call(context, store.find(data.type, data.id)); - } else { - error.call(context); + try { + store.add(data); + if (success) { + success(store.find(data.type, data.id)); + } + } catch (e) { + if (error) { + error(e); + } + } + } else if (error) { + error(); } }; -- 2.51.2 From 2a96eae7e8de11062ee28f991f847e240f5ca1c7 Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Tue, 22 Sep 2015 13:23:14 +0930 Subject: [PATCH 21/23] Add options to the AJAX Adapter's load method. --- spec/adapters/ajax/load-spec.js | 32 +++++++++++++++++++++++++++++++- src/ajax-adapter.js | 21 +++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/spec/adapters/ajax/load-spec.js b/spec/adapters/ajax/load-spec.js index f964b11..8899bba 100644 --- a/spec/adapters/ajax/load-spec.js +++ b/spec/adapters/ajax/load-spec.js @@ -265,6 +265,36 @@ test("load must call callbacks with the context provided", function (t) { server.restore(); }); -test.skip("load must use the options if they're provided"); +test("load must use the options if they're provided", function (t) { + var server = sinon.fakeServer.create({ autoRespond: false }); + var adapter = new Store.AjaxAdapter(); + var store = new Store(adapter); + t.plan(1); + t.timeoutAfter(1000); + store.define("products", {}); + server.respondWith("GET", "/products?fields[products]=title%2Cdescription&filter=foo&include=author%2Ccomments.user&page=1&sort=age%2Cname%2C-created", [ + 200, + { "Content-Type": "application/vnd.api+json" }, + JSON.stringify({ + data: [], + included: [] + }) + ]); + store.load("products", { + include: "author,comments.user", + fields: { + products: "title,description" + }, + sort: "age,name,-created", + page: 1, + filter: "foo" + }, function (products) { + t.pass("returns a successful response"); + }, function (error) { + t.fail(error); + }); + server.respond(); + server.restore(); +}); test.skip("load must throw an error if the type has not been defined", function (t) {}); diff --git a/src/ajax-adapter.js b/src/ajax-adapter.js index 5a79d07..e767f2c 100644 --- a/src/ajax-adapter.js +++ b/src/ajax-adapter.js @@ -95,6 +95,27 @@ export default class AjaxAdapter { options = options || {}; url = id ? `${this._base}/${type}/${id}` : `${this._base}/${type}`; + if (options) { + + let params = []; + + if (options.fields) { + Object.keys(options.fields).forEach(field => { + options[`fields[${field}]`] = options.fields[field]; + }); + delete options.fields; + } + + params = Object.keys(options).map(key => { + return key + "=" + encodeURIComponent(options[key]); + }).sort(); + + if (params.length) { + url = `${url}?${params.join("&")}`; + } + + } + request.open('GET', url, true); request.onload = function () { -- 2.51.2 From 6cc28491e30a0ae1f6133a4e6ca76592ffd24a52 Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Tue, 22 Sep 2015 13:44:01 +0930 Subject: [PATCH 22/23] Improve the AJAX Adapter's update method. --- spec/adapters/ajax/update-spec.js | 89 ++++++++++++++++++++++++++++++- src/ajax-adapter.js | 52 ++++++++++-------- 2 files changed, 118 insertions(+), 23 deletions(-) diff --git a/spec/adapters/ajax/update-spec.js b/spec/adapters/ajax/update-spec.js index f19f88a..e498621 100644 --- a/spec/adapters/ajax/update-spec.js +++ b/spec/adapters/ajax/update-spec.js @@ -94,8 +94,93 @@ test("update must call the error callback if an error is raised during the proce server.restore(); }); -test.skip("update must handle missing success or error callbacks", function (t) {}); +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" + } + }) + 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.skip("update must call callbacks with the context provided", function (t) {}); +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); + server.respond(); + t.equal(callback.callCount, 1); + t.equal(callback.firstCall.thisValue, context); + server.restore(); +}); test.skip("update must throw an error if the type has not been defined", function (t) {}); diff --git a/src/ajax-adapter.js b/src/ajax-adapter.js index e767f2c..c0b3c81 100644 --- a/src/ajax-adapter.js +++ b/src/ajax-adapter.js @@ -139,33 +139,43 @@ export default class AjaxAdapter { } - update(store, type, id, partial, success, error) { + update(store, type, id, partial, success, error, context) { - let request = new XMLHttpRequest(); - let data = store.convert(type, id, partial); + if (error && {}.toString.call(error) !== '[object Function]') { - request.open('PATCH', `${this._base}/${type}/${id}`, true); + this.update(store, type, id, partial, success, null, error); - request.onload = function () { - if (request.status >= 200 && request.status < 300) { - try { - store.add(data); - if (success) { - success(store.find(data.type, data.id)); - } - } catch (e) { - if (error) { - error(e); + } else if (store._types[type]) { + + let request = new XMLHttpRequest(); + let data = store.convert(type, id, partial); + + request.open('PATCH', `${this._base}/${type}/${id}`, true); + + 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); } - } else if (error) { - error(); - } - }; + }; + + request.send({ + data: JSON.stringify(data) + }); - request.send({ - data: JSON.stringify(data) - }); + } else { + throw new Error(`Unknown type '${type}'`); + } } -- 2.51.2 From 34190ec5b54a526fce94e7c25db1bfb73d84dcf0 Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Tue, 22 Sep 2015 13:45:39 +0930 Subject: [PATCH 23/23] Add errors the AJAX Adapter's CLUD methods for undefined types. --- spec/adapters/ajax/create-spec.js | 9 ++++++++- spec/adapters/ajax/destroy-spec.js | 14 ++++++++++++-- spec/adapters/ajax/load-spec.js | 9 ++++++++- spec/adapters/ajax/update-spec.js | 9 ++++++++- src/ajax-adapter.js | 12 +++++++++--- 5 files changed, 45 insertions(+), 8 deletions(-) diff --git a/spec/adapters/ajax/create-spec.js b/spec/adapters/ajax/create-spec.js index 8f1a34b..dbc742d 100644 --- a/spec/adapters/ajax/create-spec.js +++ b/spec/adapters/ajax/create-spec.js @@ -182,4 +182,11 @@ test("create must use the adapter's 'base' config if present", function (t) { server.restore(); }); -test.skip("create must throw an error if the type has not been defined", function (t) {}); +test("create must throw an error if the type has not been defined", function (t) { + var adapter = new Store.AjaxAdapter(); + var store = new Store(adapter); + t.plan(1); + t.throws(function () { + store.create("products", {}); + }, /Unknown type 'products'/); +}); diff --git a/spec/adapters/ajax/destroy-spec.js b/spec/adapters/ajax/destroy-spec.js index 36e6551..7d86595 100644 --- a/spec/adapters/ajax/destroy-spec.js +++ b/spec/adapters/ajax/destroy-spec.js @@ -84,12 +84,15 @@ test("destroy must call the error callback if an error is raised during the proc var callback = sinon.spy(); t.plan(1); t.timeoutAfter(1000); + store.define("products", {}); server.respondWith("DELETE", "/foo/1", [ 204, { "Content-Type": "application/vnd.api+json" }, "" ]); - store.destroy("foo", "1", null, callback); + store.destroy("products", "1", function () { + throw new Error(); + }, callback); server.respond(); t.equal(callback.callCount, 1); server.restore(); @@ -161,4 +164,11 @@ test("destroy must call callbacks with the context provided", function (t) { server.restore(); }); -test.skip("destroy must throw an error if the type has not been defined", function (t) {}); +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); + t.plan(1); + t.throws(function () { + store.destroy("products", "1"); + }, /Unknown type 'products'/); +}); diff --git a/spec/adapters/ajax/load-spec.js b/spec/adapters/ajax/load-spec.js index 8899bba..a9f540f 100644 --- a/spec/adapters/ajax/load-spec.js +++ b/spec/adapters/ajax/load-spec.js @@ -297,4 +297,11 @@ test("load must use the options if they're provided", function (t) { server.restore(); }); -test.skip("load must throw an error if the type has not been defined", function (t) {}); +test("load must throw an error if the type has not been defined", function (t) { + var adapter = new Store.AjaxAdapter(); + var store = new Store(adapter); + t.plan(1); + t.throws(function () { + store.load("products", "1"); + }, /Unknown type 'products'/); +}); diff --git a/spec/adapters/ajax/update-spec.js b/spec/adapters/ajax/update-spec.js index e498621..b7df7eb 100644 --- a/spec/adapters/ajax/update-spec.js +++ b/spec/adapters/ajax/update-spec.js @@ -183,4 +183,11 @@ test("update must call callbacks with the context provided", function (t) { server.restore(); }); -test.skip("update must throw an error if the type has not been defined", function (t) {}); +test("update must throw an error if the type has not been defined", function (t) { + var adapter = new Store.AjaxAdapter(); + var store = new Store(adapter); + t.plan(1); + t.throws(function () { + store.update("products", "1", {}); + }, /Unknown type 'products'/); +}); diff --git a/src/ajax-adapter.js b/src/ajax-adapter.js index c0b3c81..a2eaf1c 100644 --- a/src/ajax-adapter.js +++ b/src/ajax-adapter.js @@ -10,7 +10,7 @@ export default class AjaxAdapter { this.create(store, type, partial, success, null, error); - } else { + } else if (store._types[type]) { let request = new XMLHttpRequest(); @@ -40,6 +40,8 @@ export default class AjaxAdapter { data: JSON.stringify(store.convert(type, partial)) }); + } else { + throw new Error(`Unknown type '${type}'`); } } @@ -50,7 +52,7 @@ export default class AjaxAdapter { this.destroy(store, type, id, success, null, error); - } else { + } else if (store._types[type]) { let request = new XMLHttpRequest(); @@ -73,6 +75,8 @@ export default class AjaxAdapter { request.send(); + } else { + throw new Error(`Unknown type '${type}'`); } } @@ -87,7 +91,7 @@ export default class AjaxAdapter { 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 { + } else if (store._types[type]) { let request = new XMLHttpRequest(); let url; @@ -135,6 +139,8 @@ export default class AjaxAdapter { request.send(); + } else { + throw new Error(`Unknown type '${type}'`); } } -- 2.51.2