├── .bithoundrc ├── .envrc ├── .gitignore ├── .nvmrc ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bower.json ├── dist ├── ajax-adapter.js ├── ajax.js ├── store.dev.js ├── store.dev.js.map ├── store.js └── store.prod.js ├── docs ├── ast │ └── source │ │ ├── ajax-adapter.js.json │ │ ├── ajax.js.json │ │ ├── src │ │ ├── ajax-adapter.js.json │ │ └── store.js.json │ │ └── store.js.json ├── badge.svg ├── class │ └── src │ │ ├── ajax-adapter.js~AjaxAdapter.html │ │ └── store.js~Store.html ├── coverage.json ├── css │ ├── prettify-tomorrow.css │ └── style.css ├── dump.json ├── file │ └── src │ │ ├── ajax-adapter.js.html │ │ ├── ajax.js.html │ │ └── store.js.html ├── function │ └── index.html ├── identifiers.html ├── image │ ├── badge.svg │ ├── github.png │ └── search.png ├── index.html ├── package.json ├── script │ ├── inherited-summary.js │ ├── inner-link.js │ ├── patch-for-local.js │ ├── prettify │ │ ├── Apache-License-2.0.txt │ │ └── prettify.js │ ├── pretty-print.js │ ├── search.js │ ├── search_index.js │ └── test-summary.js └── source.html ├── esdoc.json ├── example ├── data │ ├── create-comment.json │ ├── product.json │ └── update-comment.json ├── index.html └── types │ ├── category.js │ ├── comment.js │ └── product.js ├── package.json ├── scripts ├── build ├── release └── test ├── spec ├── .eslintrc ├── adapters │ └── ajax │ │ ├── create-spec.js │ │ ├── destroy-spec.js │ │ ├── load-spec.js │ │ └── update-spec.js ├── client.js ├── server.js ├── shared.js └── store │ ├── clud │ ├── create-spec.js │ ├── destroy-spec.js │ ├── load-all-spec.js │ ├── load-spec.js │ └── update-spec.js │ ├── core │ ├── add-spec.js │ ├── convert-spec.js │ ├── define-spec.js │ ├── find-all-spec.js │ ├── find-spec.js │ ├── push-spec.js │ └── remove-spec.js │ ├── events │ ├── observable-spec.js │ ├── off-spec.js │ └── on-spec.js │ └── fields │ ├── attr-spec.js │ ├── has-many-spec.js │ └── has-one-spec.js └── src ├── .eslintrc ├── ajax-adapter.js ├── ajax.js └── store.js /.bithoundrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "dist/**", 4 | "docs/**" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | export PATH="$PATH:./node_modules/.bin" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 0.12.7 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "0.11" 5 | env: 6 | - TARGET=browser 7 | - TARGET=node 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Development Requirements 2 | 3 | - Node.js 4 | - PhantomJS 5 | 6 | ## Getting Started 7 | 8 | Clone the project and install NPM packages: 9 | 10 | ``` 11 | git clone git@github.com:haydn/json-api-store.git 12 | cd json-api-store 13 | npm install 14 | ``` 15 | 16 | ## Running Tests 17 | 18 | You can run tests once-off with NPM: 19 | 20 | ``` 21 | npm test 22 | ``` 23 | 24 | Alternatively, you can run tests in watch mode: 25 | 26 | ``` 27 | npm test watch 28 | ``` 29 | 30 | ## Generating Documentation 31 | 32 | You can regenerate the documentation with: 33 | 34 | ``` 35 | npm run docs 36 | ``` 37 | 38 | ## Building Distribution 39 | 40 | You can rebuild the the output from the source using: 41 | 42 | ``` 43 | npm run build 44 | ``` 45 | 46 | ## Making Releases 47 | 48 | There's a script available for making releases. Without the required 49 | permissions, you won't get you very far, but if you're curious here it is: 50 | 51 | ``` 52 | npm run release 53 | ``` 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Haydn Ewers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON API Store [![Build Status](https://travis-ci.org/haydn/json-api-store.svg?branch=master)](https://travis-ci.org/haydn/json-api-store) [![NPM Version](https://badge.fury.io/js/json-api-store.svg)](http://badge.fury.io/js/json-api-store) [![bitHound Score](https://www.bithound.io/github/haydn/json-api-store/badges/score.svg)](https://www.bithound.io/github/haydn/json-api-store) 2 | 3 | An isomorphic JavaScript library that acts as an in memory data store for 4 | [JSON API](http://jsonapi.org) data. Changes are 5 | broadcast using [RxJS](https://github.com/Reactive-Extensions/RxJS). Built to 6 | work with [React](https://facebook.github.io/react/). 7 | 8 | ## Installing 9 | 10 | #### NPM 11 | 12 | ``` 13 | npm i json-api-store 14 | ``` 15 | 16 | #### Bower 17 | 18 | ``` 19 | bower i json-api-store 20 | ``` 21 | 22 | #### Download 23 | 24 | To use directly in the browser you can grab the [store.prod.js](https://raw.githubusercontent.com/haydn/json-api-store/master/dist/store.prod.js) file. 25 | 26 | ## Usage 27 | 28 | ### Browser 29 | 30 | At the moment the primary use can for JSON API Store is in the browser: 31 | 32 | ```javascript 33 | 34 | // Create a new store instance. 35 | var adapter = new Store.AjaxAdapter({ base: "/api/v1" }); 36 | var store = new Store(adapter); 37 | 38 | // Define the "categories" type. 39 | store.define([ "categories", "category" ], { 40 | title: Store.attr(), 41 | products: Store.hasMany() 42 | }); 43 | 44 | // Define the "products" type. 45 | store.define([ "products", "product" ], { 46 | title: Store.attr(), 47 | category: Store.hasOne() 48 | }); 49 | 50 | // Subscribe to events using RxJS. 51 | store.observable.subscribe(function (event) { 52 | console.log(event.name, event.type, event.id, event.resource); 53 | }); 54 | 55 | // Load all the products. 56 | store.loadAll("products", { include: "category" }).subscribe(function (products) { 57 | 58 | products.length; // 1 59 | products[0].id; // "1" 60 | products[0].title; // "Example Book" 61 | products[0].category.id; // "1" 62 | products[0].category.title; // "Books" 63 | 64 | products[0] === store.find("products", "1"); // true 65 | products[0].category === store.find("categories", "1"); // true 66 | 67 | }); 68 | 69 | ``` 70 | 71 | ### Node 72 | 73 | You can also use JSON API Store in a Node.js environment (currently, there 74 | aren't any adapters that work in a Node.js): 75 | 76 | **NOTE**: Without an adapter the CLUD methods (`create`, `load`, `update` and 77 | `destroy`) cannot be used. 78 | 79 | ```javascript 80 | 81 | var Store = require("json-api-store"); 82 | 83 | var store = new Store(); 84 | 85 | store.define([ "categories", "category" ], { 86 | title: Store.attr(), 87 | products: Store.hasMany() 88 | }); 89 | 90 | store.define([ "products", "product" ], { 91 | title: Store.attr(), 92 | category: Store.hasOne() 93 | }); 94 | 95 | store.add({ 96 | type: "products", 97 | id: "1", 98 | attributes: { 99 | title: "Example Product" 100 | }, 101 | relationships: { 102 | category: { 103 | data: { 104 | type: "categories", 105 | id: "1" 106 | } 107 | } 108 | } 109 | }); 110 | 111 | store.add({ 112 | type: "categories", 113 | id: "1", 114 | attributes: { 115 | title: "Example Category" 116 | } 117 | }); 118 | 119 | store.find("products", "1").category.title; // "Example Category" 120 | 121 | ``` 122 | 123 | ## Documentation 124 | 125 | Documentation is available on the website: 126 | 127 | http://particlesystem.com/json-api-store/ 128 | 129 | ## Changelog 130 | 131 | A changelog is available on the GitHub repo: 132 | 133 | https://github.com/haydn/json-api-store/releases 134 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-api-store", 3 | "version": "0.7.0", 4 | "homepage": "https://github.com/haydn/json-api-store", 5 | "authors": [ 6 | "Haydn Ewers " 7 | ], 8 | "description": "A lightweight library for using JSON API in the browser.", 9 | "main": "dist/store.prod.js", 10 | "moduleType": [ 11 | "amd", 12 | "globals", 13 | "node" 14 | ], 15 | "license": "MIT" 16 | } 17 | -------------------------------------------------------------------------------- /dist/ajax-adapter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 8 | 9 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 10 | 11 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 12 | 13 | var _ajax = require("./ajax"); 14 | 15 | var _ajax2 = _interopRequireDefault(_ajax); 16 | 17 | var AjaxAdapter = (function () { 18 | function AjaxAdapter(options) { 19 | _classCallCheck(this, AjaxAdapter); 20 | 21 | this._base = options && options.base || ""; 22 | } 23 | 24 | _createClass(AjaxAdapter, [{ 25 | key: "create", 26 | value: function create(store, type, partial, options) { 27 | 28 | if (!store._types[type]) { 29 | throw new Error("Unknown type '" + type + "'"); 30 | } 31 | 32 | var source = (0, _ajax2["default"])({ 33 | body: JSON.stringify({ 34 | data: store.convert(type, partial) 35 | }), 36 | crossDomain: true, 37 | headers: { 38 | "Content-Type": "application/vnd.api+json" 39 | }, 40 | method: "POST", 41 | responseType: "auto", 42 | url: this._getUrl(type, null, options) 43 | })["do"](function (e) { 44 | return store.push(e.response); 45 | }).map(function (e) { 46 | return store.find(e.response.data.type, e.response.data.id); 47 | }).publish(); 48 | 49 | source.connect(); 50 | 51 | return source; 52 | } 53 | }, { 54 | key: "destroy", 55 | value: function destroy(store, type, id, options) { 56 | 57 | if (!store._types[type]) { 58 | throw new Error("Unknown type '" + type + "'"); 59 | } 60 | 61 | var source = (0, _ajax2["default"])({ 62 | crossDomain: true, 63 | headers: { 64 | "Content-Type": "application/vnd.api+json" 65 | }, 66 | method: "DELETE", 67 | responseType: "auto", 68 | url: this._getUrl(type, id, options) 69 | })["do"](function () { 70 | return store.remove(type, id); 71 | }).publish(); 72 | 73 | source.connect(); 74 | 75 | return source; 76 | } 77 | }, { 78 | key: "load", 79 | value: function load(store, type, id, options) { 80 | 81 | if (id && typeof id === "object") { 82 | return this.load(store, type, null, id); 83 | } 84 | 85 | if (!store._types[type]) { 86 | throw new Error("Unknown type '" + type + "'"); 87 | } 88 | 89 | var source = (0, _ajax2["default"])({ 90 | crossDomain: true, 91 | headers: { 92 | "Content-Type": "application/vnd.api+json" 93 | }, 94 | method: "GET", 95 | responseType: "auto", 96 | url: this._getUrl(type, id, options) 97 | })["do"](function (e) { 98 | return store.push(e.response); 99 | }).map(function () { 100 | return id ? store.find(type, id) : store.findAll(type); 101 | }).publish(); 102 | 103 | source.connect(); 104 | 105 | return source; 106 | } 107 | }, { 108 | key: "update", 109 | value: function update(store, type, id, partial, options) { 110 | 111 | if (!store._types[type]) { 112 | throw new Error("Unknown type '" + type + "'"); 113 | } 114 | 115 | var data = store.convert(type, id, partial); 116 | 117 | var source = (0, _ajax2["default"])({ 118 | body: JSON.stringify({ 119 | data: data 120 | }), 121 | crossDomain: true, 122 | headers: { 123 | "Content-Type": "application/vnd.api+json" 124 | }, 125 | method: "PATCH", 126 | responseType: "auto", 127 | url: this._getUrl(type, id, options) 128 | })["do"](function () { 129 | return store.add(data); 130 | }).map(function () { 131 | return store.find(type, id); 132 | }).publish(); 133 | 134 | source.connect(); 135 | 136 | return source; 137 | } 138 | }, { 139 | key: "_getUrl", 140 | value: function _getUrl(type, id, options) { 141 | 142 | var params = []; 143 | var url = id ? this._base + "/" + type + "/" + id : this._base + "/" + type; 144 | 145 | if (options) { 146 | 147 | if (options.fields) { 148 | Object.keys(options.fields).forEach(function (field) { 149 | options["fields[" + field + "]"] = options.fields[field]; 150 | }); 151 | delete options.fields; 152 | } 153 | 154 | params = Object.keys(options).map(function (key) { 155 | return key + "=" + encodeURIComponent(options[key]); 156 | }).sort(); 157 | 158 | if (params.length) { 159 | url = url + "?" + params.join("&"); 160 | } 161 | } 162 | 163 | return url; 164 | } 165 | }]); 166 | 167 | return AjaxAdapter; 168 | })(); 169 | 170 | exports["default"] = AjaxAdapter; 171 | module.exports = exports["default"]; 172 | -------------------------------------------------------------------------------- /dist/ajax.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 8 | 9 | var _rx = require("rx"); 10 | 11 | var _rx2 = _interopRequireDefault(_rx); 12 | 13 | /** 14 | * 15 | * Derived from RxJS-DOM, Copyright (c) Microsoft Open Technologies, Inc: 16 | * 17 | * https://github.com/Reactive-Extensions/RxJS-DOM 18 | * 19 | * The original source file can be viewed here: 20 | * 21 | * https://github.com/Reactive-Extensions/RxJS-DOM/blob/fdb169c8bd1612318d530e6e54074b1c9e537906/src/ajax.js 22 | * 23 | * Licensed under the Apache License, Version 2.0: 24 | * 25 | * http://www.apache.org/licenses/LICENSE-2.0 26 | * 27 | * Modifications from original: 28 | * 29 | * - extracted from "Rx.DOM" namespace 30 | * - minor eslinter cleanup ("var" to "let" etc) 31 | * - addition of "auto" responseType 32 | * 33 | */ 34 | 35 | var root = typeof window !== "undefined" && window || undefined; 36 | 37 | // Gets the proper XMLHttpRequest for support for older IE 38 | function getXMLHttpRequest() { 39 | if (root.XMLHttpRequest) { 40 | return new root.XMLHttpRequest(); 41 | } else { 42 | var progId = undefined; 43 | try { 44 | var progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0']; 45 | for (var i = 0; i < 3; i++) { 46 | try { 47 | progId = progIds[i]; 48 | if (new root.ActiveXObject(progId)) { 49 | break; 50 | } 51 | } catch (e) {} 52 | } 53 | return new root.ActiveXObject(progId); 54 | } catch (e) { 55 | throw new Error('XMLHttpRequest is not supported by your browser'); 56 | } 57 | } 58 | } 59 | 60 | // Get CORS support even for older IE 61 | function getCORSRequest() { 62 | var xhr = new root.XMLHttpRequest(); 63 | if ('withCredentials' in xhr) { 64 | return xhr; 65 | } else if (!!root.XDomainRequest) { 66 | return new XDomainRequest(); 67 | } else { 68 | throw new Error('CORS is not supported by your browser'); 69 | } 70 | } 71 | 72 | function normalizeAjaxSuccessEvent(e, xhr, settings) { 73 | var response = 'response' in xhr ? xhr.response : xhr.responseText; 74 | if (settings.responseType === 'auto') { 75 | try { 76 | response = JSON.parse(response); 77 | } catch (e) {} 78 | } else { 79 | response = settings.responseType === 'json' ? JSON.parse(response) : response; 80 | } 81 | return { 82 | response: response, 83 | status: xhr.status, 84 | responseType: xhr.responseType, 85 | xhr: xhr, 86 | originalEvent: e 87 | }; 88 | } 89 | 90 | function normalizeAjaxErrorEvent(e, xhr, type) { 91 | return { 92 | type: type, 93 | status: xhr.status, 94 | xhr: xhr, 95 | originalEvent: e 96 | }; 97 | } 98 | 99 | /** 100 | * Creates an observable for an Ajax request with either a settings object with url, headers, etc or a string for a URL. 101 | * 102 | * @example 103 | * source = ajax('/products'); 104 | * source = ajax({ url: 'products', method: 'GET' }); 105 | * 106 | * @param {Object} settings Can be one of the following: 107 | * 108 | * A string of the URL to make the Ajax call. 109 | * An object with the following properties 110 | * - url: URL of the request 111 | * - body: The body of the request 112 | * - method: Method of the request, such as GET, POST, PUT, PATCH, DELETE 113 | * - async: Whether the request is async 114 | * - headers: Optional headers 115 | * - crossDomain: true if a cross domain request, else false 116 | * - responseType: "text" (default), "json" or "auto" 117 | * 118 | * @returns {Observable} An observable sequence containing the XMLHttpRequest. 119 | */ 120 | 121 | exports["default"] = function (options) { 122 | var settings = { 123 | method: 'GET', 124 | crossDomain: false, 125 | async: true, 126 | headers: {}, 127 | responseType: 'text', 128 | createXHR: function createXHR() { 129 | return this.crossDomain ? getCORSRequest() : getXMLHttpRequest(); 130 | }, 131 | normalizeError: normalizeAjaxErrorEvent, 132 | normalizeSuccess: normalizeAjaxSuccessEvent 133 | }; 134 | 135 | if (typeof options === 'string') { 136 | settings.url = options; 137 | } else { 138 | for (var prop in options) { 139 | if (hasOwnProperty.call(options, prop)) { 140 | settings[prop] = options[prop]; 141 | } 142 | } 143 | } 144 | 145 | var normalizeError = settings.normalizeError; 146 | var normalizeSuccess = settings.normalizeSuccess; 147 | 148 | if (!settings.crossDomain && !settings.headers['X-Requested-With']) { 149 | settings.headers['X-Requested-With'] = 'XMLHttpRequest'; 150 | } 151 | settings.hasContent = settings.body !== undefined; 152 | 153 | return new _rx2["default"].AnonymousObservable(function (observer) { 154 | var isDone = false; 155 | var xhr; 156 | 157 | var processResponse = function processResponse(xhr, e) { 158 | var status = xhr.status === 1223 ? 204 : xhr.status; 159 | if (status >= 200 && status <= 300 || status === 0 || status === '') { 160 | observer.onNext(normalizeSuccess(e, xhr, settings)); 161 | observer.onCompleted(); 162 | } else { 163 | observer.onError(normalizeError(e, xhr, 'error')); 164 | } 165 | isDone = true; 166 | }; 167 | 168 | try { 169 | xhr = settings.createXHR();; 170 | } catch (err) { 171 | observer.onError(err); 172 | } 173 | 174 | try { 175 | if (settings.user) { 176 | xhr.open(settings.method, settings.url, settings.async, settings.user, settings.password); 177 | } else { 178 | xhr.open(settings.method, settings.url, settings.async); 179 | } 180 | 181 | var headers = settings.headers; 182 | for (var header in headers) { 183 | if (hasOwnProperty.call(headers, header)) { 184 | xhr.setRequestHeader(header, headers[header]); 185 | } 186 | } 187 | 188 | if (!!xhr.upload || !('withCredentials' in xhr) && !!root.XDomainRequest) { 189 | xhr.onload = function (e) { 190 | if (settings.progressObserver) { 191 | settings.progressObserver.onNext(e); 192 | settings.progressObserver.onCompleted(); 193 | } 194 | processResponse(xhr, e); 195 | }; 196 | 197 | if (settings.progressObserver) { 198 | xhr.onprogress = function (e) { 199 | settings.progressObserver.onNext(e); 200 | }; 201 | } 202 | 203 | xhr.onerror = function (e) { 204 | if (settings.progressObserver) { 205 | settings.progressObserver.onError(e); 206 | } 207 | observer.onError(normalizeError(e, xhr, 'error')); 208 | isDone = true; 209 | }; 210 | 211 | xhr.onabort = function (e) { 212 | if (settings.progressObserver) { 213 | settings.progressObserver.onError(e); 214 | } 215 | observer.onError(normalizeError(e, xhr, 'abort')); 216 | isDone = true; 217 | }; 218 | } else { 219 | 220 | xhr.onreadystatechange = function (e) { 221 | if (xhr.readyState === 4) { 222 | processResponse(xhr, e); 223 | } 224 | }; 225 | } 226 | 227 | xhr.send(settings.hasContent && settings.body || null); 228 | } catch (e) { 229 | observer.onError(e); 230 | } 231 | 232 | return function () { 233 | if (!isDone && xhr.readyState !== 4) { 234 | xhr.abort(); 235 | } 236 | }; 237 | }); 238 | }; 239 | 240 | ; 241 | module.exports = exports["default"]; 242 | -------------------------------------------------------------------------------- /docs/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | document 13 | document 14 | 70% 15 | 70% 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/coverage.json: -------------------------------------------------------------------------------- 1 | { 2 | "coverage": "70.37%", 3 | "expectCount": 27, 4 | "actualCount": 19, 5 | "files": { 6 | "src/ajax-adapter.js": { 7 | "expectCount": 6, 8 | "actualCount": 0, 9 | "undocumentLines": [ 10 | 3, 11 | 5, 12 | 9, 13 | 36, 14 | 59, 15 | 87 16 | ] 17 | }, 18 | "src/store.js": { 19 | "expectCount": 20, 20 | "actualCount": 18, 21 | "undocumentLines": [ 22 | 5, 23 | 117 24 | ] 25 | }, 26 | "src/ajax.js": { 27 | "expectCount": 1, 28 | "actualCount": 1, 29 | "undocumentLines": [] 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /docs/css/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: #718c00; } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: #8959a8; } 17 | 18 | /* a comment */ 19 | .com { 20 | color: #8e908c; } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: #4271ae; } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: #f5871f; } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #4d4d4c; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #4d4d4c; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #4d4d4c; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /docs/file/src/ajax-adapter.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | src/ajax-adapter.js | API Document 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | Home 17 | Identifier 18 | Source 19 | 20 | Repository 21 | 28 |
29 | 30 | 55 | 56 |

src/ajax-adapter.js

57 |
import ajax from "./ajax";
 58 | 
 59 | export default class AjaxAdapter {
 60 | 
 61 |   constructor(options) {
 62 |     this._base = (options && options.base) || "";
 63 |   };
 64 | 
 65 |   create(store, type, partial, options) {
 66 | 
 67 |     if (!store._types[type]) {
 68 |       throw new Error(`Unknown type '${type}'`);
 69 |     }
 70 | 
 71 |     let source = ajax({
 72 |       body: JSON.stringify({
 73 |         data: store.convert(type, partial)
 74 |       }),
 75 |       crossDomain: true,
 76 |       headers: {
 77 |         "Content-Type": "application/vnd.api+json"
 78 |       },
 79 |       method: "POST",
 80 |       responseType: "auto",
 81 |       url: this._getUrl(type, null, options)
 82 |     }).do(e => store.push(e.response))
 83 |       .map(e => store.find(e.response.data.type, e.response.data.id))
 84 |       .publish();
 85 | 
 86 |     source.connect();
 87 | 
 88 |     return source;
 89 | 
 90 |   }
 91 | 
 92 |   destroy(store, type, id, options) {
 93 | 
 94 |     if (!store._types[type]) {
 95 |       throw new Error(`Unknown type '${type}'`);
 96 |     }
 97 | 
 98 |     let source = ajax({
 99 |       crossDomain: true,
100 |       headers: {
101 |         "Content-Type": "application/vnd.api+json"
102 |       },
103 |       method: "DELETE",
104 |       responseType: "auto",
105 |       url: this._getUrl(type, id, options)
106 |     }).do(() => store.remove(type, id))
107 |       .publish();
108 | 
109 |     source.connect();
110 | 
111 |     return source;
112 | 
113 |   }
114 | 
115 |   load(store, type, id, options) {
116 | 
117 |     if (id && typeof id === "object") {
118 |       return this.load(store, type, null, id);
119 |     }
120 | 
121 |     if (!store._types[type]) {
122 |       throw new Error(`Unknown type '${type}'`);
123 |     }
124 | 
125 |     let source = ajax({
126 |       crossDomain: true,
127 |       headers: {
128 |         "Content-Type": "application/vnd.api+json"
129 |       },
130 |       method: "GET",
131 |       responseType: "auto",
132 |       url: this._getUrl(type, id, options)
133 |     }).do(e => store.push(e.response))
134 |       .map(() => id ? store.find(type, id) : store.findAll(type))
135 |       .publish();
136 | 
137 |     source.connect();
138 | 
139 |     return source;
140 | 
141 |   }
142 | 
143 |   update(store, type, id, partial, options) {
144 | 
145 |     if (!store._types[type]) {
146 |       throw new Error(`Unknown type '${type}'`);
147 |     }
148 | 
149 |     let data = store.convert(type, id, partial);
150 | 
151 |     let source = ajax({
152 |       body: JSON.stringify({
153 |         data: data
154 |       }),
155 |       crossDomain: true,
156 |       headers: {
157 |         "Content-Type": "application/vnd.api+json"
158 |       },
159 |       method: "PATCH",
160 |       responseType: "auto",
161 |       url: this._getUrl(type, id, options)
162 |     }).do(() => store.add(data))
163 |       .map(() => store.find(type, id))
164 |       .publish();
165 | 
166 |     source.connect();
167 | 
168 |     return source;
169 | 
170 |   }
171 | 
172 |   _getUrl(type, id, options) {
173 | 
174 |     let params = [];
175 |     let url = id ? `${this._base}/${type}/${id}` : `${this._base}/${type}`;
176 | 
177 |     if (options) {
178 | 
179 |       if (options.fields) {
180 |         Object.keys(options.fields).forEach(field => {
181 |           options[`fields[${field}]`] = options.fields[field];
182 |         });
183 |         delete options.fields;
184 |       }
185 | 
186 |       params = Object.keys(options).map(key => {
187 |         return key + "=" + encodeURIComponent(options[key]);
188 |       }).sort();
189 | 
190 |       if (params.length) {
191 |         url = `${url}?${params.join("&")}`;
192 |       }
193 | 
194 |     }
195 | 
196 |     return url;
197 | 
198 |   }
199 | 
200 | }
201 | 
202 | 203 |
204 | 205 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /docs/file/src/ajax.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | src/ajax.js | API Document 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | Home 17 | Identifier 18 | Source 19 | 20 | Repository 21 | 28 |
29 | 30 | 55 | 56 |

src/ajax.js

57 |
import Rx from "rx";
 58 | 
 59 | /**
 60 |  *
 61 |  * Derived from RxJS-DOM, Copyright (c) Microsoft Open Technologies, Inc:
 62 |  *
 63 |  * https://github.com/Reactive-Extensions/RxJS-DOM
 64 |  *
 65 |  * The original source file can be viewed here:
 66 |  *
 67 |  * https://github.com/Reactive-Extensions/RxJS-DOM/blob/fdb169c8bd1612318d530e6e54074b1c9e537906/src/ajax.js
 68 |  *
 69 |  * Licensed under the Apache License, Version 2.0:
 70 |  *
 71 |  * http://www.apache.org/licenses/LICENSE-2.0
 72 |  *
 73 |  * Modifications from original:
 74 |  *
 75 |  * - extracted from "Rx.DOM" namespace
 76 |  * - minor eslinter cleanup ("var" to "let" etc)
 77 |  * - addition of "auto" responseType
 78 |  *
 79 |  */
 80 | 
 81 | var root = (typeof window !== "undefined" && window) || this;
 82 | 
 83 | // Gets the proper XMLHttpRequest for support for older IE
 84 | function getXMLHttpRequest() {
 85 |   if (root.XMLHttpRequest) {
 86 |     return new root.XMLHttpRequest();
 87 |   } else {
 88 |     let progId;
 89 |     try {
 90 |       let progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'];
 91 |       for(let i = 0; i < 3; i++) {
 92 |         try {
 93 |           progId = progIds[i];
 94 |           if (new root.ActiveXObject(progId)) {
 95 |             break;
 96 |           }
 97 |         } catch(e) { }
 98 |       }
 99 |       return new root.ActiveXObject(progId);
100 |     } catch (e) {
101 |       throw new Error('XMLHttpRequest is not supported by your browser');
102 |     }
103 |   }
104 | }
105 | 
106 | // Get CORS support even for older IE
107 | function getCORSRequest() {
108 |   var xhr = new root.XMLHttpRequest();
109 |   if ('withCredentials' in xhr) {
110 |     return xhr;
111 |   } else if (!!root.XDomainRequest) {
112 |     return new XDomainRequest();
113 |   } else {
114 |     throw new Error('CORS is not supported by your browser');
115 |   }
116 | }
117 | 
118 | function normalizeAjaxSuccessEvent(e, xhr, settings) {
119 |   var response = ('response' in xhr) ? xhr.response : xhr.responseText;
120 |   if (settings.responseType === 'auto') {
121 |     try {
122 |       response = JSON.parse(response);
123 |     } catch (e) {}
124 |   } else {
125 |     response = settings.responseType === 'json' ? JSON.parse(response) : response;
126 |   }
127 |   return {
128 |     response: response,
129 |     status: xhr.status,
130 |     responseType: xhr.responseType,
131 |     xhr: xhr,
132 |     originalEvent: e
133 |   };
134 | }
135 | 
136 | function normalizeAjaxErrorEvent(e, xhr, type) {
137 |   return {
138 |     type: type,
139 |     status: xhr.status,
140 |     xhr: xhr,
141 |     originalEvent: e
142 |   };
143 | }
144 | 
145 | /**
146 |  * Creates an observable for an Ajax request with either a settings object with url, headers, etc or a string for a URL.
147 |  *
148 |  * @example
149 |  *   source = ajax('/products');
150 |  *   source = ajax({ url: 'products', method: 'GET' });
151 |  *
152 |  * @param {Object} settings Can be one of the following:
153 |  *
154 |  *  A string of the URL to make the Ajax call.
155 |  *  An object with the following properties
156 |  *   - url: URL of the request
157 |  *   - body: The body of the request
158 |  *   - method: Method of the request, such as GET, POST, PUT, PATCH, DELETE
159 |  *   - async: Whether the request is async
160 |  *   - headers: Optional headers
161 |  *   - crossDomain: true if a cross domain request, else false
162 |  *   - responseType: "text" (default), "json" or "auto"
163 |  *
164 |  * @returns {Observable} An observable sequence containing the XMLHttpRequest.
165 | */
166 | export default function (options) {
167 |   var settings = {
168 |     method: 'GET',
169 |     crossDomain: false,
170 |     async: true,
171 |     headers: {},
172 |     responseType: 'text',
173 |     createXHR: function(){
174 |       return this.crossDomain ? getCORSRequest() : getXMLHttpRequest();
175 |     },
176 |     normalizeError: normalizeAjaxErrorEvent,
177 |     normalizeSuccess: normalizeAjaxSuccessEvent
178 |   };
179 | 
180 |   if(typeof options === 'string') {
181 |     settings.url = options;
182 |   } else {
183 |     for(let prop in options) {
184 |       if(hasOwnProperty.call(options, prop)) {
185 |         settings[prop] = options[prop];
186 |       }
187 |     }
188 |   }
189 | 
190 |   let normalizeError = settings.normalizeError;
191 |   let normalizeSuccess = settings.normalizeSuccess;
192 | 
193 |   if (!settings.crossDomain && !settings.headers['X-Requested-With']) {
194 |     settings.headers['X-Requested-With'] = 'XMLHttpRequest';
195 |   }
196 |   settings.hasContent = settings.body !== undefined;
197 | 
198 |   return new Rx.AnonymousObservable(function (observer) {
199 |     var isDone = false;
200 |     var xhr;
201 | 
202 |     var processResponse = function(xhr, e){
203 |       var status = xhr.status === 1223 ? 204 : xhr.status;
204 |       if ((status >= 200 && status <= 300) || status === 0 || status === '') {
205 |         observer.onNext(normalizeSuccess(e, xhr, settings));
206 |         observer.onCompleted();
207 |       } else {
208 |         observer.onError(normalizeError(e, xhr, 'error'));
209 |       }
210 |       isDone = true;
211 |     };
212 | 
213 |     try {
214 |       xhr = settings.createXHR();;
215 |     } catch (err) {
216 |       observer.onError(err);
217 |     }
218 | 
219 |     try {
220 |       if (settings.user) {
221 |         xhr.open(settings.method, settings.url, settings.async, settings.user, settings.password);
222 |       } else {
223 |         xhr.open(settings.method, settings.url, settings.async);
224 |       }
225 | 
226 |       let headers = settings.headers;
227 |       for (let header in headers) {
228 |         if (hasOwnProperty.call(headers, header)) {
229 |           xhr.setRequestHeader(header, headers[header]);
230 |         }
231 |       }
232 | 
233 |       if(!!xhr.upload || (!('withCredentials' in xhr) && !!root.XDomainRequest)) {
234 |         xhr.onload = function(e) {
235 |           if(settings.progressObserver) {
236 |             settings.progressObserver.onNext(e);
237 |             settings.progressObserver.onCompleted();
238 |           }
239 |           processResponse(xhr, e);
240 |         };
241 | 
242 |         if(settings.progressObserver) {
243 |           xhr.onprogress = function(e) {
244 |             settings.progressObserver.onNext(e);
245 |           };
246 |         }
247 | 
248 |         xhr.onerror = function(e) {
249 |           if (settings.progressObserver) {
250 |             settings.progressObserver.onError(e);
251 |           }
252 |           observer.onError(normalizeError(e, xhr, 'error'));
253 |           isDone = true;
254 |         };
255 | 
256 |         xhr.onabort = function(e) {
257 |           if (settings.progressObserver) {
258 |             settings.progressObserver.onError(e);
259 |           }
260 |           observer.onError(normalizeError(e, xhr, 'abort'));
261 |           isDone = true;
262 |         };
263 |       } else {
264 | 
265 |         xhr.onreadystatechange = function (e) {
266 |           if (xhr.readyState === 4) {
267 |             processResponse(xhr, e);
268 |           }
269 |         };
270 |       }
271 | 
272 |       xhr.send(settings.hasContent && settings.body || null);
273 |     } catch (e) {
274 |       observer.onError(e);
275 |     }
276 | 
277 |     return function () {
278 |       if (!isDone && xhr.readyState !== 4) { xhr.abort(); }
279 |     };
280 |   });
281 | };
282 | 
283 | 284 |
285 | 286 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | -------------------------------------------------------------------------------- /docs/function/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Function | API Document 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | Home 17 | Identifier 18 | Source 19 | 20 | Repository 21 | 28 |
29 | 30 | 55 | 56 |

Function

57 |
58 | 59 | 60 | 61 | 62 | 69 | 81 | 85 | 86 | 87 |
Static Public Summary
63 | public 64 | 65 | 66 | 67 | 68 | 70 |
71 |

72 | ajax(settings: Object): Observable 73 |

74 |
75 |
76 | 77 | 78 |

Creates an observable for an Ajax request with either a settings object with url, headers, etc or a string for a URL.

79 |
80 |
82 | 83 | 84 |
88 |
89 |

Static Public

90 | 91 |
92 |

93 | public 94 | 95 | 96 | 97 | 98 | ajax(settings: Object): Observable 99 | 100 | 101 | 102 | source 103 | 104 |

105 | 106 |
import ajax from 'json-api-store/src/ajax.js'
107 | 108 | 109 |

Creates an observable for an Ajax request with either a settings object with url, headers, etc or a string for a URL.

110 |
111 | 112 | 113 | 114 |
115 |

Params:

116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 139 | 140 | 141 |
NameTypeAttributeDescription
settingsObject

Can be one of the following:

127 |

A string of the URL to make the Ajax call. 128 | An object with the following properties

129 |
    130 |
  • url: URL of the request
  • 131 |
  • body: The body of the request
  • 132 |
  • method: Method of the request, such as GET, POST, PUT, PATCH, DELETE
  • 133 |
  • async: Whether the request is async
  • 134 |
  • headers: Optional headers
  • 135 |
  • crossDomain: true if a cross domain request, else false
  • 136 |
  • responseType: "text" (default), "json" or "auto"
  • 137 |
138 |
142 |
143 |
144 | 145 |
146 |

Return:

147 | 148 | 149 | 150 | 152 | 153 |
Observable

An observable sequence containing the XMLHttpRequest.

151 |
154 |
155 |
156 |
157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 |
165 |

Example:

166 | 167 |
168 | 169 |
  source = ajax('/products');
170 |   source = ajax({ url: 'products', method: 'GET' });
171 |
172 |
173 | 174 | 175 | 176 | 177 | 178 |
179 |
180 |
181 | 182 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /docs/identifiers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Index | API Document 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | Home 17 | Identifier 18 | Source 19 | 20 | Repository 21 | 28 |
29 | 30 | 55 | 56 |

Identifier

57 |

Class Summary

58 | 59 | 60 | 61 | 62 | 69 | 81 | 85 | 86 | 87 | 94 | 106 | 110 | 111 | 112 |
Static Public Class Summary
63 | public 64 | 65 | 66 | 67 | 68 | 70 |
71 |

72 | AjaxAdapter 73 |

74 |
75 |
76 | 77 | 78 | 79 |
80 |
82 | 83 | 84 |
88 | public 89 | 90 | 91 | 92 | 93 | 95 |
96 |

97 | Store 98 |

99 |
100 |
101 | 102 | 103 | 104 |
105 |
107 | 108 | 109 |
113 |
114 | 115 |

Function Summary

116 | 117 | 118 | 119 | 120 | 127 | 139 | 143 | 144 | 145 |
Static Public Function Summary
121 | public 122 | 123 | 124 | 125 | 126 | 128 |
129 |

130 | ajax(settings: Object): Observable 131 |

132 |
133 |
134 | 135 | 136 |

Creates an observable for an Ajax request with either a settings object with url, headers, etc or a string for a URL.

137 |
138 |
140 | 141 | 142 |
146 |
147 | 148 | 149 |
150 | 151 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /docs/image/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | document 13 | document 14 | @ratio@ 15 | @ratio@ 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/image/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haydn/json-api-store/fc36a38e7c37b276198daa00e7a285adeda67c41/docs/image/github.png -------------------------------------------------------------------------------- /docs/image/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haydn/json-api-store/fc36a38e7c37b276198daa00e7a285adeda67c41/docs/image/search.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | API Document 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | Home 17 | Identifier 18 | Source 19 | 20 | Repository 21 | 28 |
29 | 30 | 55 | 56 |

JSON API Store Build Status NPM Version bitHound Score

57 |

An isomorphic JavaScript library that acts as an in memory data store for 58 | JSON API data. Changes are 59 | broadcast using RxJS. Built to 60 | work with React.

61 |

Usage

62 |

Browser

63 |

At the moment the primary use can for JSON API Store is in the browser:

64 |

 65 | // Create a new store instance.
 66 | var adapter = new Store.AjaxAdapter({ base: "/api/v1" });
 67 | var store = new Store(adapter);
 68 | 
 69 | // Define the "categories" type.
 70 | store.define([ "categories", "category" ], {
 71 |   title: Store.attr(),
 72 |   products: Store.hasMany()
 73 | });
 74 | 
 75 | // Define the "products" type.
 76 | store.define([ "products", "product" ], {
 77 |   title: Store.attr(),
 78 |   category: Store.hasOne()
 79 | });
 80 | 
 81 | // Subscribe to events using RxJS.
 82 | store.observable.subscribe(function (event) {
 83 |   console.log(event.name, event.type, event.id, event.resource);
 84 | });
 85 | 
 86 | // Load all the products.
 87 | store.load("products", { include: "category" }, function (products) {
 88 | 
 89 |   products.length; // 1
 90 |   products[0].id; // "1"
 91 |   products[0].title; // "Example Book"
 92 |   products[0].category.id; // "1"
 93 |   products[0].category.title; // "Books"
 94 | 
 95 |   products[0] === store.find("products", "1"); // true
 96 |   products[0].category === store.find("categories", "1"); // true
 97 | 
 98 | });
 99 | 
100 |

Node

101 |

You can also use JSON API Store in a Node.js environment (currently, there 102 | aren't any adapters that work in a Node.js):

103 |

NOTE: Without an adapter the CLUD methods (create, load, update and 104 | destroy) cannot be used.

105 |

106 | var Store = require("json-api-store");
107 | 
108 | var store = new Store();
109 | 
110 | store.define([ "categories", "category" ], {
111 |   title: Store.attr(),
112 |   products: Store.hasMany()
113 | });
114 | 
115 | store.define([ "products", "product" ], {
116 |   title: Store.attr(),
117 |   category: Store.hasOne()
118 | });
119 | 
120 | store.add({
121 |   type: "products",
122 |   id: "1",
123 |   attributes: {
124 |     title: "Example Product"
125 |   },
126 |   relationships: {
127 |     category: {
128 |       data: {
129 |         type: "categories",
130 |         id: "1"
131 |       }
132 |     }
133 |   }
134 | });
135 | 
136 | store.add({
137 |   type: "categories",
138 |   id: "1",
139 |   attributes: {
140 |     title: "Example Category"
141 |   }
142 | });
143 | 
144 | store.find("products", "1").category.title; // "Example Category"
145 | 
146 |

Documentation

147 |

Full documentation is available on the website:

148 |

http://particlesystem.com/json-api-store/

149 |

Installing

150 |

NPM

151 |
npm i json-api-store
152 | 

Bower

153 |
bower i json-api-store
154 | 

Download

155 |

To use directly in the browser you can grab the store.prod.js file.

156 |
157 |
158 | 159 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-api-store", 3 | "version": "0.6.1", 4 | "description": "An isomorphic JavaScript library that acts as an in memory data store for JSON API data.", 5 | "repository": "haydn/json-api-store", 6 | "main": "dist/store.js", 7 | "scripts": { 8 | "build": "./scripts/build", 9 | "docs": "./node_modules/.bin/esdoc -c esdoc.json", 10 | "release": "./scripts/release", 11 | "test": "./scripts/test" 12 | }, 13 | "author": "Haydn Ewers", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "babel": "^5.8.21", 17 | "babelify": "^6.2.0", 18 | "browser-run": "2.5.0", 19 | "browserify": "^11.0.1", 20 | "chokidar-cli": "^1.0.1", 21 | "esdoc": "^0.2.2", 22 | "eslint": "^1.3.1", 23 | "exorcist": "^0.4.0", 24 | "faucet": "0.0.1", 25 | "sinon": "^1.16.1", 26 | "tape-catch": "^1.0.4", 27 | "tape-run": "^1.1.0", 28 | "tape": "^4.2.0", 29 | "uglifyify": "^3.0.1" 30 | }, 31 | "dependencies": { 32 | "array.prototype.find": "^1.0.0", 33 | "rx": "^4.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docs/script/inherited-summary.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | function toggle(ev) { 3 | var button = ev.target; 4 | var parent = ev.target.parentElement; 5 | while(parent) { 6 | if (parent.tagName === 'TABLE' && parent.classList.contains('summary')) break; 7 | parent = parent.parentElement; 8 | } 9 | 10 | if (!parent) return; 11 | 12 | var tbody = parent.querySelector('tbody'); 13 | if (button.classList.contains('opened')) { 14 | button.classList.remove('opened'); 15 | button.classList.add('closed'); 16 | tbody.style.display = 'none'; 17 | } else { 18 | button.classList.remove('closed'); 19 | button.classList.add('opened'); 20 | tbody.style.display = 'block'; 21 | } 22 | } 23 | 24 | var buttons = document.querySelectorAll('.inherited-summary thead .toggle'); 25 | for (var i = 0; i < buttons.length; i++) { 26 | buttons[i].addEventListener('click', toggle); 27 | } 28 | })(); 29 | -------------------------------------------------------------------------------- /docs/script/inner-link.js: -------------------------------------------------------------------------------- 1 | // inner link(#foo) can not correctly scroll, because page has fixed header, 2 | // so, I manually scroll. 3 | (function(){ 4 | var matched = location.hash.match(/errorLines=([\d,]+)/); 5 | if (matched) return; 6 | 7 | function adjust() { 8 | window.scrollBy(0, -55); 9 | var el = document.querySelector('.inner-link-active'); 10 | if (el) el.classList.remove('inner-link-active'); 11 | 12 | var el = document.querySelector(location.hash); 13 | if (el) el.classList.add('inner-link-active'); 14 | } 15 | 16 | window.addEventListener('hashchange', adjust); 17 | 18 | if (location.hash) { 19 | setTimeout(adjust, 0); 20 | } 21 | })(); 22 | -------------------------------------------------------------------------------- /docs/script/patch-for-local.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | if (location.protocol === 'file:') { 3 | var elms = document.querySelectorAll('a[href="./"]'); 4 | for (var i = 0; i < elms.length; i++) { 5 | elms[i].href = './index.html'; 6 | } 7 | } 8 | })(); 9 | -------------------------------------------------------------------------------- /docs/script/prettify/Apache-License-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /docs/script/prettify/prettify.js: -------------------------------------------------------------------------------- 1 | var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; 2 | (function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= 3 | [],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), 9 | l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, 10 | q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, 11 | q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, 12 | "");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), 13 | a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} 14 | for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], 18 | "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], 19 | H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], 20 | J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ 21 | I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), 22 | ["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", 23 | /^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), 24 | ["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", 25 | hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= 26 | !k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p' + pair[2] + ''); 35 | } 36 | } 37 | 38 | var innerHTML = ''; 39 | for (kind in html) { 40 | var list = html[kind]; 41 | if (!list.length) continue; 42 | innerHTML += '
  • ' + kind + '
  • \n' + list.join('\n'); 43 | } 44 | result.innerHTML = innerHTML; 45 | if (innerHTML) result.style.display = 'block'; 46 | selectedIndex = -1; 47 | }); 48 | 49 | // down, up and enter key are pressed, select search result. 50 | input.addEventListener('keydown', function(ev){ 51 | if (ev.keyCode === 40) { 52 | // arrow down 53 | var current = result.children[selectedIndex]; 54 | var selected = result.children[selectedIndex + 1]; 55 | if (selected && selected.classList.contains('search-separator')) { 56 | var selected = result.children[selectedIndex + 2]; 57 | selectedIndex++; 58 | } 59 | 60 | if (selected) { 61 | if (current) current.classList.remove('selected'); 62 | selectedIndex++; 63 | selected.classList.add('selected'); 64 | } 65 | } else if (ev.keyCode === 38) { 66 | // arrow up 67 | var current = result.children[selectedIndex]; 68 | var selected = result.children[selectedIndex - 1]; 69 | if (selected && selected.classList.contains('search-separator')) { 70 | var selected = result.children[selectedIndex - 2]; 71 | selectedIndex--; 72 | } 73 | 74 | if (selected) { 75 | if (current) current.classList.remove('selected'); 76 | selectedIndex--; 77 | selected.classList.add('selected'); 78 | } 79 | } else if (ev.keyCode === 13) { 80 | // enter 81 | var current = result.children[selectedIndex]; 82 | if (current) { 83 | var link = current.querySelector('a'); 84 | if (link) location.href = link.href; 85 | } 86 | } else { 87 | return; 88 | } 89 | 90 | ev.preventDefault(); 91 | }); 92 | 93 | // select search result when search result is mouse over. 94 | result.addEventListener('mousemove', function(ev){ 95 | var current = result.children[selectedIndex]; 96 | if (current) current.classList.remove('selected'); 97 | 98 | var li = ev.target; 99 | while (li) { 100 | if (li.nodeName === 'LI') break; 101 | li = li.parentElement; 102 | } 103 | 104 | if (li) { 105 | selectedIndex = Array.prototype.indexOf.call(result.children, li); 106 | li.classList.add('selected'); 107 | } 108 | }); 109 | 110 | // clear search result when body is clicked. 111 | document.body.addEventListener('click', function(ev){ 112 | selectedIndex = -1; 113 | result.style.display = 'none'; 114 | result.innerHTML = ''; 115 | }); 116 | 117 | })(); 118 | -------------------------------------------------------------------------------- /docs/script/test-summary.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | function toggle(ev) { 3 | var button = ev.target; 4 | var parent = ev.target.parentElement; 5 | while(parent) { 6 | if (parent.tagName === 'TR' && parent.classList.contains('test-describe')) break; 7 | parent = parent.parentElement; 8 | } 9 | 10 | if (!parent) return; 11 | 12 | var direction; 13 | if (button.classList.contains('opened')) { 14 | button.classList.remove('opened'); 15 | button.classList.add('closed'); 16 | direction = 'closed'; 17 | } else { 18 | button.classList.remove('closed'); 19 | button.classList.add('opened'); 20 | direction = 'opened'; 21 | } 22 | 23 | var targetDepth = parseInt(parent.dataset.testDepth, 10) + 1; 24 | var nextElement = parent.nextElementSibling; 25 | while (nextElement) { 26 | var depth = parseInt(nextElement.dataset.testDepth, 10); 27 | if (depth >= targetDepth) { 28 | if (direction === 'opened') { 29 | if (depth === targetDepth) nextElement.style.display = ''; 30 | } else if (direction === 'closed') { 31 | nextElement.style.display = 'none'; 32 | var innerButton = nextElement.querySelector('.toggle'); 33 | if (innerButton && innerButton.classList.contains('opened')) { 34 | innerButton.classList.remove('opened'); 35 | innerButton.classList.add('closed'); 36 | } 37 | } 38 | } else { 39 | break; 40 | } 41 | nextElement = nextElement.nextElementSibling; 42 | } 43 | } 44 | 45 | var buttons = document.querySelectorAll('.test-summary tr.test-describe .toggle'); 46 | for (var i = 0; i < buttons.length; i++) { 47 | buttons[i].addEventListener('click', toggle); 48 | } 49 | 50 | var topDescribes = document.querySelectorAll('.test-summary tr[data-test-depth="0"]'); 51 | for (var i = 0; i < topDescribes.length; i++) { 52 | topDescribes[i].style.display = ''; 53 | } 54 | })(); 55 | -------------------------------------------------------------------------------- /docs/source.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Source | API Document 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
    16 | Home 17 | Identifier 18 | Source 19 | 20 | Repository 21 | 28 |
    29 | 30 | 55 | 56 |

    Source 19/27

    57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 |
    FileIdentifierDocumentSizeLinesUpdated
    src/ajax-adapter.jsAjaxAdapter0 %0/63003 byte1442015-10-04 23:04:31 (UTC)
    src/ajax.jsajax100 %1/16218 byte2252015-10-04 23:04:31 (UTC)
    src/store.jsStore90 %18/2024674 byte7032015-10-04 23:04:31 (UTC)
    97 |
    98 | 99 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./docs" 4 | } 5 | -------------------------------------------------------------------------------- /example/data/create-comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "type": "comments", 4 | "id": "2", 5 | "attributes": { 6 | "body": "Yeah, it's rad!" 7 | }, 8 | "relationships": { 9 | "product": { 10 | "data": { "type": "products", "id": "1" } 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/data/product.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "type": "products", 4 | "id": "1", 5 | "attributes": { 6 | "title": "A Book", 7 | "description": "A really good book." 8 | }, 9 | "relationships": { 10 | "categories": { 11 | "data": [ 12 | { "type": "categories", "id": "1" }, 13 | { "type": "categories", "id": "2" }, 14 | { "type": "categories", "id": "3" } 15 | ] 16 | }, 17 | "comments": { 18 | "data": [ 19 | { "type": "comments", "id": "1" } 20 | ] 21 | }, 22 | "related": { 23 | "data": [ 24 | { "type": "products", "id": "2" } 25 | ] 26 | } 27 | } 28 | }, 29 | "included": [ 30 | { 31 | "type": "categories", 32 | "id": "1", 33 | "attributes": { 34 | "name": "Books" 35 | } 36 | }, 37 | { 38 | "type": "categories", 39 | "id": "2", 40 | "attributes": { 41 | "name": "Popular" 42 | } 43 | }, 44 | { 45 | "type": "categories", 46 | "id": "3", 47 | "attributes": { 48 | "name": "New" 49 | } 50 | }, 51 | { 52 | "type": "comments", 53 | "id": "1", 54 | "attributes": { 55 | "body": "I love this book!" 56 | } 57 | }, 58 | { 59 | "type": "products", 60 | "id": "2", 61 | "attributes": { 62 | "title": "Another Book" 63 | }, 64 | "relationships": { 65 | "categories": { 66 | "data": [ 67 | { "type": "categories", "id": "1" } 68 | ] 69 | }, 70 | "comments": { 71 | "data": [] 72 | }, 73 | "related": { 74 | "data": [] 75 | } 76 | } 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /example/data/update-comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "type": "comments", 4 | "id": "1", 5 | "attributes": { 6 | "body": "I hate this book!" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 | 18 |
    19 | Loading... 20 |
    21 |
    22 |
    23 | 24 | 25 | 26 |
    27 | 28 | 29 | 30 | 31 | 32 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /example/types/category.js: -------------------------------------------------------------------------------- 1 | var Category = { 2 | // Maps the "name" attribute in the data to the "title" property in the store. 3 | title: Store.attr("name"), 4 | products: Store.hasMany() 5 | }; 6 | -------------------------------------------------------------------------------- /example/types/comment.js: -------------------------------------------------------------------------------- 1 | var Comment = { 2 | body: Store.attr(), 3 | product: Store.hasOne() 4 | }; 5 | -------------------------------------------------------------------------------- /example/types/product.js: -------------------------------------------------------------------------------- 1 | var Product = { 2 | title: Store.attr(), 3 | description: Store.attr(), 4 | categories: Store.hasMany(), 5 | comments: Store.hasMany(), 6 | // This relationship is one-way (no inverse relationship). 7 | relatedProducts: Store.hasMany("related") 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-api-store", 3 | "version": "0.7.0", 4 | "description": "An isomorphic JavaScript library that acts as an in memory data store for JSON API data.", 5 | "repository": "haydn/json-api-store", 6 | "main": "dist/store.js", 7 | "scripts": { 8 | "build": "./scripts/build", 9 | "docs": "./node_modules/.bin/esdoc -c esdoc.json", 10 | "release": "./scripts/release", 11 | "test": "./scripts/test" 12 | }, 13 | "author": "Haydn Ewers", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "babel": "^5.8.21", 17 | "babelify": "^6.2.0", 18 | "browser-run": "2.5.0", 19 | "browserify": "^11.0.1", 20 | "chokidar-cli": "^1.0.1", 21 | "esdoc": "^0.2.2", 22 | "eslint": "^1.3.1", 23 | "exorcist": "^0.4.0", 24 | "faucet": "0.0.1", 25 | "sinon": "^1.16.1", 26 | "tape-catch": "^1.0.4", 27 | "tape-run": "^1.1.0", 28 | "tape": "^4.2.0", 29 | "uglifyify": "^3.0.1" 30 | }, 31 | "dependencies": { 32 | "array.prototype.find": "^1.0.0", 33 | "rx": "^4.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Node 4 | ./node_modules/.bin/babel src/store.js -m common --module-id Store -o dist/store.js 5 | ./node_modules/.bin/babel src/ajax.js -m common --module-id ajax -o dist/ajax.js 6 | ./node_modules/.bin/babel src/ajax-adapter.js -m common --module-id AjaxAdapter -o dist/ajax-adapter.js 7 | 8 | # Browser (Development) 9 | ./node_modules/.bin/browserify src/store.js -d -s Store -t babelify | ./node_modules/.bin/exorcist dist/store.dev.js.map -b dist > dist/store.dev.js 10 | 11 | # Browser (Production) 12 | ./node_modules/.bin/browserify src/store.js -s Store -t babelify -t uglifyify -o dist/store.prod.js 13 | -------------------------------------------------------------------------------- /scripts/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $(git branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/') != "master" ]; then 4 | echo >&2 "Looks like you're not on the master branch. Checkout master and run this script." 5 | exit 1 6 | fi 7 | 8 | if [[ -n $(git status --porcelain 2> /dev/null) ]]; then 9 | echo >&2 "Looks like you're in a dirty head state. Clean-up un-committed files before and run this script again." 10 | exit 1 11 | fi 12 | 13 | echo "$ git pull" 14 | git pull 15 | 16 | echo "$ npm install" 17 | npm install 18 | 19 | echo "$ npm test node && npm test browser" 20 | npm test node && npm test browser 21 | 22 | if [ $? -ne 0 ]; then 23 | echo >&2 "Looks like there are failing tests. Make sure all tests pass and run this script again." 24 | exit 1 25 | fi 26 | 27 | echo "$ eslint --quiet src" 28 | eslint --quiet src 29 | 30 | if [ $? -ne 0 ]; then 31 | echo >&2 "Looks like there are linting errors. Make sure all errors are cleaned-up and run this script again." 32 | exit 1 33 | fi 34 | 35 | echo "Existing releases:" 36 | git tag -l 37 | 38 | read -p "Enter the version number for this release (eg '1.4.5'): " version 39 | 40 | echo "Building docs..." 41 | npm run docs 42 | echo "Building dist..." 43 | npm run build 44 | echo "Updating NPM and Bower manifests..." 45 | sed -i.bak -E "s/\"version\": \"[0-9]+\.[0-9]+\.[0-9]+\"/\"version\": \"$version\"/" package.json 46 | rm package.json.bak 47 | sed -i.bak -E "s/\"version\": \"[0-9]+\.[0-9]+\.[0-9]+\"/\"version\": \"$version\"/" bower.json 48 | rm bower.json.bak 49 | 50 | read -p "Are you sure you want to release v$version? (yes/no) " confirm 51 | 52 | if [ $confirm != "yes" ]; then 53 | echo "Aborting." 54 | exit 0 55 | fi 56 | 57 | echo "$ git add ." 58 | git add . 59 | echo "$ git commit -a -m \"Release v$version.\"" 60 | git commit -a -m "Release v$version." 61 | echo "$ git tag \"v$version\"" 62 | git tag "v$version" 63 | echo "$ git push" 64 | git push 65 | echo "$ git push --tags" 66 | git push --tags 67 | echo "$ npm publish" 68 | npm publish 69 | 70 | echo "Updating the website..." 71 | 72 | tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/release.XXXX") 73 | 74 | # Copy dist, docs & example to a temp dir. 75 | cp -R dist "$tmpdir/dist" 76 | cp -R docs "$tmpdir/docs" 77 | cp -R example "$tmpdir/example" 78 | # Checkout the GitHub pages branch. 79 | echo "$ git checkout gh-pages" 80 | git checkout gh-pages 81 | # Delete the current dist & example dirs. 82 | rm -r dist example 83 | # Copy across the new dist, docs & example dirs. 84 | cp -R "$tmpdir"/dist dist 85 | cp -R "$tmpdir"/dist "releases/v$version" 86 | cp -R "$tmpdir"/docs "docs/v$version" 87 | cp -R "$tmpdir"/example example 88 | # Clean-up the temp dir. 89 | rm -r $tmpdir 90 | # Fix mentions of the version number. 91 | sed -i.bak -E "s/v[0-9]+\.[0-9]+\.[0-9]+/v$version/g" index.html 92 | rm index.html.bak 93 | # Add, commit & push. 94 | echo "$ git add ." 95 | git add . 96 | echo "$ git commit -a -m \"Release v$version.\"" 97 | git commit -a -m "Release v$version." 98 | echo "$ git push" 99 | git push 100 | 101 | git checkout master 102 | 103 | echo 104 | echo "Done. All that's left to do is add details to the release notes on GitHub." 105 | echo 106 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## Travis CI 4 | 5 | if [ "$TARGET" = "browser" ] 6 | then 7 | ./node_modules/.bin/browserify spec/client.js -t babelify | node_modules/.bin/tape-run 8 | exit $? 9 | elif [ "$TARGET" = "node" ] 10 | then 11 | ./node_modules/.bin/babel-node spec/server.js 12 | exit $? 13 | fi 14 | 15 | ## Development 16 | 17 | if [ "$2" = "watch" ] 18 | then 19 | ./node_modules/.bin/chokidar './src/**/*.js' './spec/**/*.js' -c "./scripts/test $1" 20 | exit 0 21 | else 22 | if [ "$1" = "browser" ] 23 | then 24 | set -o pipefail 25 | echo "== TESTING IN BROWSER ==" 26 | ./node_modules/.bin/browserify spec/client.js -d -t babelify | node_modules/.bin/tape-run | node_modules/.bin/faucet 27 | exit $? 28 | elif [ "$1" = "node" ] 29 | then 30 | set -o pipefail 31 | echo "== TESTING IN NODE ==" 32 | ./node_modules/.bin/babel-node spec/server.js | node_modules/.bin/faucet 33 | exit $? 34 | else 35 | echo "Target not specified. Usage examples:" 36 | echo " npm test node" 37 | echo " npm test browser watch" 38 | exit 0 39 | fi 40 | fi 41 | -------------------------------------------------------------------------------- /spec/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "ecmaFeatures": { 7 | "modules": true 8 | }, 9 | "rules": { 10 | "key-spacing": 0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spec/adapters/ajax/create-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import sinon from "sinon"; 3 | import Store from "../../../src/store"; 4 | 5 | test("create must post a resource to the server and add it to the store on success", function (t) { 6 | var server = sinon.fakeServer.create({ autoRespond: false }); 7 | var adapter = new Store.AjaxAdapter(); 8 | var store = new Store(adapter); 9 | t.plan(3); 10 | t.timeoutAfter(1000); 11 | store.define("products", { 12 | title: Store.attr() 13 | }); 14 | server.respondWith("POST", "/products", function (request) { 15 | t.deepEqual(JSON.parse(request.requestBody), { 16 | data: { 17 | type: "products", 18 | attributes: { 19 | title: "My Book" 20 | }, 21 | relationships: {} 22 | } 23 | }); 24 | var data = { 25 | data: { 26 | type: "products", 27 | id: "9", 28 | attributes: { 29 | title: "My Book" 30 | } 31 | } 32 | }; 33 | request.respond(201, { "Content-Type": "application/vnd.api+json" }, JSON.stringify(data)); 34 | }); 35 | store.create("products", { title: "My Book" }).subscribe(function (product) { 36 | t.equal(product.title, "My Book"); 37 | t.equal(store.find("products", "9").title, "My Book"); 38 | }); 39 | server.respond(); 40 | server.restore(); 41 | }); 42 | 43 | test("create must handle 500 errors for failed attempts", function (t) { 44 | var server = sinon.fakeServer.create({ autoRespond: false }); 45 | var adapter = new Store.AjaxAdapter(); 46 | var store = new Store(adapter); 47 | t.plan(2); 48 | t.timeoutAfter(1000); 49 | store.define("products", {}); 50 | server.respondWith("POST", "/products", [ 51 | 500, 52 | { "Content-Type": "application/vnd.api+json" }, 53 | "" 54 | ]); 55 | t.equal(store.find("products").length, 0); 56 | store.create("products", {}).subscribe(function () { 57 | t.fail("must not call the success callback"); 58 | }, function () { 59 | t.equal(store.find("products").length, 0); 60 | }); 61 | server.respond(); 62 | server.restore(); 63 | }); 64 | 65 | test("create must call the error callback if an error is raised during the process", function (t) { 66 | var server = sinon.fakeServer.create({ autoRespond: false }); 67 | var adapter = new Store.AjaxAdapter(); 68 | var store = new Store(adapter); 69 | var callback = sinon.spy(); 70 | t.plan(1); 71 | t.timeoutAfter(1000); 72 | store.define("products", {}); 73 | server.respondWith("POST", "/products", [ 74 | 201, 75 | { 76 | "Content-Type": "application/vnd.api+json" 77 | }, 78 | JSON.stringify({ 79 | data: { 80 | type: "products", 81 | id: "9" 82 | }, 83 | included: [ 84 | { 85 | type: "foo", 86 | id: "1" 87 | } 88 | ] 89 | }) 90 | ]); 91 | store.create("products", {}).subscribe(function () {}, callback); 92 | server.respond(); 93 | t.equal(callback.callCount, 1); 94 | server.restore(); 95 | }); 96 | 97 | test("create must use the adapter's 'base' config if present", function (t) { 98 | var server = sinon.fakeServer.create({ autoRespond: false }); 99 | var adapter = new Store.AjaxAdapter({ base: "http://example.com" }); 100 | var store = new Store(adapter); 101 | t.plan(1); 102 | t.timeoutAfter(1000); 103 | store.define("products", {}); 104 | server.respondWith("POST", "http://example.com/products", [ 105 | 201, 106 | { "Content-Type": "application/vnd.api+json" }, 107 | JSON.stringify({ 108 | data: { type: "products", id: "9" } 109 | }) 110 | ]); 111 | store.create("products", {}).subscribe(function (product) { 112 | t.equal(store.find("products", "9"), product); 113 | }); 114 | server.respond(); 115 | server.restore(); 116 | }); 117 | 118 | test("create must use the options if they're provided", function (t) { 119 | var server = sinon.fakeServer.create({ autoRespond: false }); 120 | var adapter = new Store.AjaxAdapter(); 121 | var store = new Store(adapter); 122 | t.plan(1); 123 | t.timeoutAfter(1000); 124 | store.define("products", {}); 125 | server.respondWith("POST", "/products?fields[products]=title%2Cdescription&filter=foo&include=author%2Ccomments.user&page=1&sort=age%2Cname%2C-created", [ 126 | 201, 127 | { "Content-Type": "application/vnd.api+json" }, 128 | JSON.stringify({ 129 | data: { type: "products", id: "9" }, 130 | included: [] 131 | }) 132 | ]); 133 | store.create("products", {}, { 134 | include: "author,comments.user", 135 | fields: { 136 | products: "title,description" 137 | }, 138 | sort: "age,name,-created", 139 | page: 1, 140 | filter: "foo" 141 | }).subscribe(function (product) { 142 | t.pass("returns a successful response"); 143 | }, function (error) { 144 | t.fail(error); 145 | }); 146 | server.respond(); 147 | server.restore(); 148 | }); 149 | 150 | test("create must throw an error if the type has not been defined", function (t) { 151 | var adapter = new Store.AjaxAdapter(); 152 | var store = new Store(adapter); 153 | t.plan(1); 154 | t.throws(function () { 155 | store.create("products", {}); 156 | }, /Unknown type 'products'/); 157 | }); 158 | 159 | test("create must use the correct content type header", function (t) { 160 | var server = sinon.fakeServer.create({ autoRespond: false }); 161 | var adapter = new Store.AjaxAdapter(); 162 | var store = new Store(adapter); 163 | t.plan(1); 164 | t.timeoutAfter(1000); 165 | store.define("products", {}); 166 | server.respondWith("POST", "/products", function (request) { 167 | t.notEqual(request.requestHeaders["Content-Type"].split(";").indexOf("application/vnd.api+json"), -1); 168 | }); 169 | store.create("products", {}); 170 | server.respond(); 171 | server.restore(); 172 | }); 173 | -------------------------------------------------------------------------------- /spec/adapters/ajax/destroy-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import sinon from "sinon"; 3 | import Store from "../../../src/store"; 4 | 5 | test("destroy must delete a resource from the server and remove it from the store on success", function (t) { 6 | var server = sinon.fakeServer.create({ autoRespond: false }); 7 | var adapter = new Store.AjaxAdapter(); 8 | var store = new Store(adapter); 9 | t.plan(3); 10 | t.timeoutAfter(1000); 11 | store.define("products", {}); 12 | server.respondWith("DELETE", "/products/6", function (request) { 13 | t.false(request.requestBody); 14 | request.respond(204, { "Content-Type": "application/vnd.api+json" }, ""); 15 | }); 16 | store.add({ 17 | type: "products", 18 | id: "6" 19 | }); 20 | t.equal(store.find("products").length, 1); 21 | store.destroy("products", "6").subscribe(function () { 22 | t.equal(store.find("products").length, 0); 23 | }); 24 | server.respond(); 25 | server.restore(); 26 | }); 27 | 28 | test("destroy must handle 500 errors for failed attempts", function (t) { 29 | var server = sinon.fakeServer.create({ autoRespond: false }); 30 | var adapter = new Store.AjaxAdapter(); 31 | var store = new Store(adapter); 32 | t.plan(2); 33 | t.timeoutAfter(1000); 34 | store.define("products", {}); 35 | server.respondWith("DELETE", "/products/6", [ 36 | 500, 37 | { 38 | "Content-Type": "application/vnd.api+json" 39 | }, 40 | "" 41 | ]); 42 | store.add({ 43 | type: "products", 44 | id: "6" 45 | }); 46 | t.equal(store.find("products").length, 1); 47 | store.destroy("products", "6").subscribe(function () { 48 | t.fail("must not call the success callback"); 49 | }, function () { 50 | t.equal(store.find("products").length, 1); 51 | }); 52 | server.respond(); 53 | server.restore(); 54 | }); 55 | 56 | test("destroy must use the adapter's 'base' config if present", function (t) { 57 | var server = sinon.fakeServer.create({ autoRespond: false }); 58 | var adapter = new Store.AjaxAdapter({ base: "http://example.com" }); 59 | var store = new Store(adapter); 60 | t.plan(2); 61 | t.timeoutAfter(1000); 62 | store.define("products", {}); 63 | server.respondWith("DELETE", "http://example.com/products/2", [ 64 | 204, 65 | { "Content-Type": "application/vnd.api+json" }, 66 | "" 67 | ]); 68 | store.add({ type: "products", id: "2" }); 69 | t.equal(store.find("products").length, 1); 70 | store.destroy("products", "2").subscribe(function () { 71 | t.equal(store.find("products").length, 0); 72 | }); 73 | server.respond(); 74 | server.restore(); 75 | }); 76 | 77 | test("destroy must call the error callback if an error is raised during the process", function (t) { 78 | var server = sinon.fakeServer.create({ autoRespond: false }); 79 | var adapter = new Store.AjaxAdapter(); 80 | var store = new Store(adapter); 81 | var callback = sinon.spy(); 82 | t.plan(1); 83 | t.timeoutAfter(1000); 84 | store.define("products", {}); 85 | server.respondWith("DELETE", "/foo/1", [ 86 | 204, 87 | { "Content-Type": "application/vnd.api+json" }, 88 | "" 89 | ]); 90 | store.destroy("products", "1").subscribe(function () { 91 | throw new Error(); 92 | }, callback); 93 | server.respond(); 94 | t.equal(callback.callCount, 1); 95 | server.restore(); 96 | }); 97 | 98 | test("destroy must use the options if they're provided", function (t) { 99 | var server = sinon.fakeServer.create({ autoRespond: false }); 100 | var adapter = new Store.AjaxAdapter(); 101 | var store = new Store(adapter); 102 | t.plan(1); 103 | t.timeoutAfter(1000); 104 | store.define("products", {}); 105 | server.respondWith("DELETE", "/products/1?fields[products]=title%2Cdescription&filter=foo&include=author%2Ccomments.user&page=1&sort=age%2Cname%2C-created", [ 106 | 204, 107 | { "Content-Type": "application/vnd.api+json" }, 108 | "" 109 | ]); 110 | store.destroy("products", "1", { 111 | include: "author,comments.user", 112 | fields: { 113 | products: "title,description" 114 | }, 115 | sort: "age,name,-created", 116 | page: 1, 117 | filter: "foo" 118 | }).subscribe(function (product) { 119 | t.pass("returns a successful response"); 120 | }, function (error) { 121 | t.fail(error); 122 | }); 123 | server.respond(); 124 | server.restore(); 125 | }); 126 | 127 | test("destroy must throw an error if the type has not been defined", function (t) { 128 | var adapter = new Store.AjaxAdapter(); 129 | var store = new Store(adapter); 130 | t.plan(1); 131 | t.throws(function () { 132 | store.destroy("products", "1"); 133 | }, /Unknown type 'products'/); 134 | }); 135 | 136 | test("destroy must use the correct content type header", function (t) { 137 | var server = sinon.fakeServer.create({ autoRespond: false }); 138 | var adapter = new Store.AjaxAdapter(); 139 | var store = new Store(adapter); 140 | t.plan(1); 141 | t.timeoutAfter(1000); 142 | store.define("products", {}); 143 | server.respondWith("DELETE", "/products/6", function (request) { 144 | t.notEqual(request.requestHeaders["Content-Type"].split(";").indexOf("application/vnd.api+json"), -1); 145 | }); 146 | store.destroy("products", "6"); 147 | server.respond(); 148 | server.restore(); 149 | }); 150 | -------------------------------------------------------------------------------- /spec/adapters/ajax/load-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import sinon from "sinon"; 3 | import Store from "../../../src/store"; 4 | 5 | test("load must fetch a single resource from the server and add it to the store", function (t) { 6 | var server = sinon.fakeServer.create({ autoRespond: false }); 7 | var adapter = new Store.AjaxAdapter(); 8 | var store = new Store(adapter); 9 | t.plan(6); 10 | t.timeoutAfter(1000); 11 | store.define("products", { 12 | title: Store.attr(), 13 | category: Store.hasOne(), 14 | comments: Store.hasMany() 15 | }); 16 | store.define("categories", {}); 17 | store.define("comments", {}); 18 | server.respondWith("GET", "/products/12", function (request) { 19 | t.false(request.requestBody); 20 | var data = { 21 | data: { 22 | type: "products", 23 | id: "12", 24 | attributes: { 25 | title: "An Awesome Book" 26 | }, 27 | relationships: { 28 | category: { 29 | data: { 30 | id: "6", 31 | type: "categories" 32 | } 33 | }, 34 | comments: { 35 | data: [ 36 | { 37 | id: "2", 38 | type: "comments" 39 | }, 40 | { 41 | id: "4", 42 | type: "comments" 43 | } 44 | ] 45 | } 46 | } 47 | } 48 | }; 49 | request.respond(200, { "Content-Type": "application/vnd.api+json" }, JSON.stringify(data)); 50 | }); 51 | store.load("products", "12").subscribe(function (product) { 52 | t.equal(store.find("products", "12"), product); 53 | t.equal(store.find("products", "12").title, "An Awesome Book"); 54 | t.equal(store.find("products", "12").category, store.find("categories", "6")); 55 | t.deepEqual(store.find("products", "12").comments.map(c => c.id).sort(), [ "2", "4" ]); 56 | t.deepEqual(store.find("products", "12").comments.map(c => c.type).sort(), [ "comments", "comments" ]); 57 | }); 58 | server.respond(); 59 | server.restore(); 60 | }); 61 | 62 | test("load must fetch a collection of resources from the server and add them to the store", function (t) { 63 | var server = sinon.fakeServer.create({ autoRespond: false }); 64 | var adapter = new Store.AjaxAdapter(); 65 | var store = new Store(adapter); 66 | t.plan(3); 67 | t.timeoutAfter(1000); 68 | store.define("products", { 69 | title: Store.attr() 70 | }); 71 | server.respondWith("GET", "/products", [ 72 | 200, 73 | { 74 | "Content-Type": "application/vnd.api+json" 75 | }, 76 | JSON.stringify({ 77 | data: [ 78 | { 79 | type: "products", 80 | id: "2", 81 | attributes: { 82 | title: "A Book" 83 | } 84 | }, 85 | { 86 | type: "products", 87 | id: "4", 88 | attributes: { 89 | title: "B Book" 90 | } 91 | }, 92 | { 93 | type: "products", 94 | id: "7", 95 | attributes: { 96 | title: "C Book" 97 | } 98 | } 99 | ] 100 | }) 101 | ]); 102 | store.load("products").subscribe(function (products) { 103 | t.equal(products.length, 3); 104 | t.equal(store.find("products").length, 3); 105 | t.deepEqual(store.find("products").map(a => a.title).sort(), [ "A Book", "B Book", "C Book" ]); 106 | }, function (error) { 107 | t.fail(error); 108 | }); 109 | server.respond(); 110 | server.restore(); 111 | }); 112 | 113 | test("load must handle 500 errors for failed attempts", function (t) { 114 | var server = sinon.fakeServer.create({ autoRespond: false }); 115 | var adapter = new Store.AjaxAdapter(); 116 | var store = new Store(adapter); 117 | t.plan(2); 118 | t.timeoutAfter(1000); 119 | store.define("products", {}); 120 | server.respondWith("GET", "/products/12", [ 121 | 500, 122 | { "Content-Type": "application/vnd.api+json" }, 123 | "" 124 | ]); 125 | t.equal(store.find("products").length, 0); 126 | store.load("products", "12").subscribe(function () { 127 | t.fail("must not call the success callback"); 128 | }, function () { 129 | t.equal(store.find("products").length, 0); 130 | }); 131 | server.respond(); 132 | server.restore(); 133 | }); 134 | 135 | test("load must use the adapter's 'base' config if present", function (t) { 136 | var server = sinon.fakeServer.create({ autoRespond: false }); 137 | var adapter = new Store.AjaxAdapter({ base: "http://example.com" }); 138 | var store = new Store(adapter); 139 | t.plan(3); 140 | t.timeoutAfter(1000); 141 | store.define("products", {}); 142 | server.respondWith("GET", "http://example.com/products/9", [ 143 | 200, 144 | { "Content-Type": "application/vnd.api+json" }, 145 | JSON.stringify({ 146 | data: { type: "products", id: "9" } 147 | }) 148 | ]); 149 | server.respondWith("GET", "http://example.com/products", [ 150 | 200, 151 | { "Content-Type": "application/vnd.api+json" }, 152 | JSON.stringify({ 153 | data: [ 154 | { type: "products", id: "2" }, 155 | { type: "products", id: "4" }, 156 | { type: "products", id: "7" } 157 | ] 158 | }) 159 | ]); 160 | t.equal(store.find("products").length, 0); 161 | store.load("products", "9").subscribe(function () { 162 | t.equal(store.find("products").length, 1); 163 | store.load("products").subscribe(function () { 164 | t.deepEqual(store.find("products").map(x => x.id).sort(), [ "2", "4", "7", "9" ]); 165 | }); 166 | server.respond(); 167 | }); 168 | server.respond(); 169 | server.restore(); 170 | }); 171 | 172 | test("load must call the error callback if an error is raised during the process", function (t) { 173 | var server = sinon.fakeServer.create({ autoRespond: false }); 174 | var adapter = new Store.AjaxAdapter(); 175 | var store = new Store(adapter); 176 | var callback = sinon.spy(); 177 | t.plan(1); 178 | t.timeoutAfter(1000); 179 | store.define("products", {}); 180 | server.respondWith("GET", "/products/1", [ 181 | 200, 182 | { "Content-Type": "application/vnd.api+json" }, 183 | JSON.stringify({ 184 | data: { type: "products", id: "1" }, 185 | included: [ 186 | { type: "foo", id: "1" }, 187 | ] 188 | }) 189 | ]); 190 | store.load("products", "1").subscribe(function () {}, callback); 191 | server.respond(); 192 | t.equal(callback.callCount, 1); 193 | server.restore(); 194 | }); 195 | 196 | test("load must use the options if they're provided", function (t) { 197 | var server = sinon.fakeServer.create({ autoRespond: false }); 198 | var adapter = new Store.AjaxAdapter(); 199 | var store = new Store(adapter); 200 | t.plan(1); 201 | t.timeoutAfter(1000); 202 | store.define("products", {}); 203 | server.respondWith("GET", "/products?fields[products]=title%2Cdescription&filter=foo&include=author%2Ccomments.user&page=1&sort=age%2Cname%2C-created", [ 204 | 200, 205 | { "Content-Type": "application/vnd.api+json" }, 206 | JSON.stringify({ 207 | data: [], 208 | included: [] 209 | }) 210 | ]); 211 | store.load("products", { 212 | include: "author,comments.user", 213 | fields: { 214 | products: "title,description" 215 | }, 216 | sort: "age,name,-created", 217 | page: 1, 218 | filter: "foo" 219 | }).subscribe(function (products) { 220 | t.pass("returns a successful response"); 221 | }, function (error) { 222 | t.fail(error); 223 | }); 224 | server.respond(); 225 | server.restore(); 226 | }); 227 | 228 | test("load must throw an error if the type has not been defined", function (t) { 229 | var adapter = new Store.AjaxAdapter(); 230 | var store = new Store(adapter); 231 | t.plan(1); 232 | t.throws(function () { 233 | store.load("products", "1"); 234 | }, /Unknown type 'products'/); 235 | }); 236 | 237 | test("load must use the correct content type header", function (t) { 238 | var server = sinon.fakeServer.create({ autoRespond: false }); 239 | var adapter = new Store.AjaxAdapter(); 240 | var store = new Store(adapter); 241 | t.plan(1); 242 | t.timeoutAfter(1000); 243 | store.define("products", {}); 244 | server.respondWith("GET", "/products/6", function (request) { 245 | t.notEqual(request.requestHeaders["Content-Type"].split(";").indexOf("application/vnd.api+json"), -1); 246 | }); 247 | store.load("products", "6"); 248 | server.respond(); 249 | server.restore(); 250 | }); 251 | -------------------------------------------------------------------------------- /spec/adapters/ajax/update-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import sinon from "sinon"; 3 | import Store from "../../../src/store"; 4 | 5 | test("update must update a resource on the server and add reflect the changes in the store on success", function (t) { 6 | var server = sinon.fakeServer.create({ autoRespond: false }); 7 | var adapter = new Store.AjaxAdapter(); 8 | var store = new Store(adapter); 9 | t.plan(3); 10 | t.timeoutAfter(1000); 11 | store.define("products", { 12 | title: Store.attr() 13 | }); 14 | server.respondWith("PATCH", "/products/9", function (request) { 15 | t.deepEqual(JSON.parse(request.requestBody), { 16 | data: { 17 | type: "products", 18 | id: "9", 19 | attributes: { 20 | title: "My Book!" 21 | }, 22 | relationships: {} 23 | } 24 | }); 25 | request.respond(204, { "Content-Type": "application/vnd.api+json" }, ""); 26 | }); 27 | store.add({ 28 | type: "products", 29 | id: "9", 30 | attributes: { 31 | title: "My Book" 32 | } 33 | }); 34 | store.update("products", "9", { title: "My Book!" }).subscribe(function (product) { 35 | t.equal(product.title, "My Book!"); 36 | t.equal(store.find("products", "9").title, "My Book!"); 37 | }); 38 | server.respond(); 39 | server.restore(); 40 | }); 41 | 42 | test("update must handle 500 errors for failed attempts", function (t) { 43 | var server = sinon.fakeServer.create({ autoRespond: false }); 44 | var adapter = new Store.AjaxAdapter(); 45 | var store = new Store(adapter); 46 | t.plan(2); 47 | t.timeoutAfter(1000); 48 | store.define("products", {}); 49 | server.respondWith("PATCH", "/products/12", [ 50 | 500, 51 | { "Content-Type": "application/vnd.api+json" }, 52 | "" 53 | ]); 54 | t.equal(store.find("products").length, 0); 55 | store.update("products", "12", {}).subscribe(function () { 56 | t.fail("must not call the success callback"); 57 | }, function () { 58 | t.equal(store.find("products").length, 0); 59 | }); 60 | server.respond(); 61 | server.restore(); 62 | }); 63 | 64 | test("update must use the adapter's 'base' config if present", function (t) { 65 | var server = sinon.fakeServer.create({ autoRespond: false }); 66 | var adapter = new Store.AjaxAdapter({ base: "http://example.com" }); 67 | var store = new Store(adapter); 68 | t.plan(1); 69 | t.timeoutAfter(1000); 70 | store.define("products", {}); 71 | server.respondWith("PATCH", "http://example.com/products/9", [ 72 | 204, 73 | { "Content-Type": "application/vnd.api+json" }, 74 | "" 75 | ]); 76 | store.add({ type: "products", id: "9" }); 77 | store.update("products", "9", {}).subscribe(function () { 78 | t.pass("should call success callback"); 79 | }); 80 | server.respond(); 81 | server.restore(); 82 | }); 83 | 84 | test("update must call the error callback if an error is raised during the process", function (t) { 85 | var server = sinon.fakeServer.create({ autoRespond: false }); 86 | var adapter = new Store.AjaxAdapter(); 87 | var store = new Store(adapter); 88 | var callback = sinon.spy(); 89 | t.plan(1); 90 | t.timeoutAfter(1000); 91 | store.define("products", { 92 | foo: { 93 | deserialize: function () { 94 | throw new Error(); 95 | }, 96 | serialize: function () {} 97 | } 98 | }); 99 | server.respondWith("PATCH", "/products/1", [ 100 | 204, 101 | { "Content-Type": "application/vnd.api+json" }, 102 | "" 103 | ]); 104 | store.update("products", "1", {}).subscribe(function () {}, callback); 105 | server.respond(); 106 | t.equal(callback.callCount, 1); 107 | server.restore(); 108 | }); 109 | 110 | test("update must use the options if they're provided", function (t) { 111 | var server = sinon.fakeServer.create({ autoRespond: false }); 112 | var adapter = new Store.AjaxAdapter(); 113 | var store = new Store(adapter); 114 | t.plan(1); 115 | t.timeoutAfter(1000); 116 | store.define("products", {}); 117 | server.respondWith("PATCH", "/products/1?fields[products]=title%2Cdescription&filter=foo&include=author%2Ccomments.user&page=1&sort=age%2Cname%2C-created", [ 118 | 201, 119 | { "Content-Type": "application/vnd.api+json" }, 120 | "" 121 | ]); 122 | store.update("products", "1", {}, { 123 | include: "author,comments.user", 124 | fields: { 125 | products: "title,description" 126 | }, 127 | sort: "age,name,-created", 128 | page: 1, 129 | filter: "foo" 130 | }).subscribe(function (product) { 131 | t.pass("returns a successful response"); 132 | }, function (error) { 133 | t.fail(error); 134 | }); 135 | server.respond(); 136 | server.restore(); 137 | }); 138 | 139 | test("update must throw an error if the type has not been defined", function (t) { 140 | var adapter = new Store.AjaxAdapter(); 141 | var store = new Store(adapter); 142 | t.plan(1); 143 | t.throws(function () { 144 | store.update("products", "1", {}); 145 | }, /Unknown type 'products'/); 146 | }); 147 | 148 | test("update must use the correct content type header", function (t) { 149 | var server = sinon.fakeServer.create({ autoRespond: false }); 150 | var adapter = new Store.AjaxAdapter(); 151 | var store = new Store(adapter); 152 | t.plan(1); 153 | t.timeoutAfter(1000); 154 | store.define("products", {}); 155 | server.respondWith("PATCH", "/products/4", function (request) { 156 | t.notEqual(request.requestHeaders["Content-Type"].split(";").indexOf("application/vnd.api+json"), -1); 157 | }); 158 | store.update("products", "4", {}); 159 | server.respond(); 160 | server.restore(); 161 | }); 162 | -------------------------------------------------------------------------------- /spec/client.js: -------------------------------------------------------------------------------- 1 | import "./adapters/ajax/create-spec"; 2 | import "./adapters/ajax/destroy-spec"; 3 | import "./adapters/ajax/load-spec"; 4 | import "./adapters/ajax/update-spec"; 5 | import "./shared"; 6 | -------------------------------------------------------------------------------- /spec/server.js: -------------------------------------------------------------------------------- 1 | import "./shared"; 2 | -------------------------------------------------------------------------------- /spec/shared.js: -------------------------------------------------------------------------------- 1 | if (!Function.prototype.bind) { 2 | Function.prototype.bind = function(oThis) { 3 | if (typeof this !== 'function') { 4 | // closest thing possible to the ECMAScript 5 5 | // internal IsCallable function 6 | throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); 7 | } 8 | 9 | var aArgs = Array.prototype.slice.call(arguments, 1), 10 | fToBind = this, 11 | fNOP = function() {}, 12 | fBound = function() { 13 | return fToBind.apply(this instanceof fNOP 14 | ? this 15 | : oThis, 16 | aArgs.concat(Array.prototype.slice.call(arguments))); 17 | }; 18 | 19 | if (this.prototype) { 20 | // native functions don't have a prototype 21 | fNOP.prototype = this.prototype; 22 | } 23 | fBound.prototype = new fNOP(); 24 | 25 | return fBound; 26 | }; 27 | } 28 | 29 | import "./store/clud/create-spec"; 30 | import "./store/clud/destroy-spec"; 31 | import "./store/clud/load-all-spec"; 32 | import "./store/clud/load-spec"; 33 | import "./store/clud/update-spec"; 34 | import "./store/core/add-spec"; 35 | import "./store/core/convert-spec"; 36 | import "./store/core/define-spec"; 37 | import "./store/core/find-spec"; 38 | import "./store/core/find-all-spec"; 39 | import "./store/core/push-spec"; 40 | import "./store/core/remove-spec"; 41 | import "./store/events/observable-spec"; 42 | import "./store/events/off-spec"; 43 | import "./store/events/on-spec"; 44 | import "./store/fields/attr-spec"; 45 | import "./store/fields/has-many-spec"; 46 | import "./store/fields/has-one-spec"; 47 | -------------------------------------------------------------------------------- /spec/store/clud/create-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import sinon from "sinon"; 3 | import Store from "../../../src/store"; 4 | 5 | test("create must throw an error if it is called when there isn't an adapter", function (t) { 6 | var store = new Store(); 7 | t.plan(1); 8 | t.throws(function () { 9 | store.create(); 10 | }, /Adapter missing\. Specify an adapter when creating the store: `var store = new Store\(adapter\);`/); 11 | }); 12 | 13 | test("create must call the create method provided by the adapter", function (t) { 14 | var a = {}; 15 | var adatper = { 16 | create: sinon.spy(function () { 17 | return a; 18 | }) 19 | }; 20 | var store = new Store(adatper); 21 | var type = "foo"; 22 | var partial = {}; 23 | var options = {}; 24 | var result; 25 | t.plan(6); 26 | t.doesNotThrow(function () { 27 | result = store.create(type, partial, options); 28 | }, "should not throw an error"); 29 | t.equal(adatper.create.lastCall.args[0], store); 30 | t.equal(adatper.create.lastCall.args[1], type); 31 | t.equal(adatper.create.lastCall.args[2], partial); 32 | t.equal(adatper.create.lastCall.args[3], options); 33 | t.equal(result, a); 34 | }); 35 | -------------------------------------------------------------------------------- /spec/store/clud/destroy-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import sinon from "sinon"; 3 | import Store from "../../../src/store"; 4 | 5 | test("destroy must throw an error if it is called when there isn't an adapter", function (t) { 6 | var store = new Store(); 7 | t.plan(1); 8 | t.throws(function () { 9 | store.destroy(); 10 | }, /Adapter missing\. Specify an adapter when creating the store: `var store = new Store\(adapter\);`/); 11 | }); 12 | 13 | test("destroy must call the destroy method provided by the adapter", function (t) { 14 | var a = {}; 15 | var adatper = { 16 | destroy: sinon.spy(function () { 17 | return a; 18 | }) 19 | }; 20 | var store = new Store(adatper); 21 | var type = "foo"; 22 | var id = "1"; 23 | var options = {}; 24 | var result; 25 | t.plan(6); 26 | t.doesNotThrow(function () { 27 | result = store.destroy(type, id, options); 28 | }, "should not throw an error"); 29 | t.equal(adatper.destroy.lastCall.args[0], store); 30 | t.equal(adatper.destroy.lastCall.args[1], type); 31 | t.equal(adatper.destroy.lastCall.args[2], id); 32 | t.equal(adatper.destroy.lastCall.args[3], options); 33 | t.equal(result, a); 34 | }); 35 | -------------------------------------------------------------------------------- /spec/store/clud/load-all-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import sinon from "sinon"; 3 | import Store from "../../../src/store"; 4 | 5 | test("loadAll must throw an error if it is called when there isn't an adapter", function (t) { 6 | var store = new Store(); 7 | t.plan(1); 8 | t.throws(function () { 9 | store.loadAll(); 10 | }, /Adapter missing\. Specify an adapter when creating the store: `var store = new Store\(adapter\);`/); 11 | }); 12 | 13 | test("loadAll must call the load method provided by the adapter", function (t) { 14 | var a = {}; 15 | var adatper = { 16 | load: sinon.spy(function () { 17 | return a; 18 | }) 19 | }; 20 | var store = new Store(adatper); 21 | var type = "foo"; 22 | var options = {}; 23 | var result; 24 | t.plan(6); 25 | t.doesNotThrow(function () { 26 | result = store.loadAll(type, options); 27 | }, "should not throw an error"); 28 | t.equal(adatper.load.lastCall.args[0], store); 29 | t.equal(adatper.load.lastCall.args[1], type); 30 | t.equal(adatper.load.lastCall.args[2], null); 31 | t.equal(adatper.load.lastCall.args[3], options); 32 | t.equal(result, a); 33 | }); 34 | -------------------------------------------------------------------------------- /spec/store/clud/load-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import sinon from "sinon"; 3 | import Store from "../../../src/store"; 4 | 5 | test("load must throw an error if it is called when there isn't an adapter", function (t) { 6 | var store = new Store(); 7 | t.plan(1); 8 | t.throws(function () { 9 | store.load(); 10 | }, /Adapter missing\. Specify an adapter when creating the store: `var store = new Store\(adapter\);`/); 11 | }); 12 | 13 | test("load must call the load method provided by the adapter", function (t) { 14 | var a = {}; 15 | var adatper = { 16 | load: sinon.spy(function () { 17 | return a; 18 | }) 19 | }; 20 | var store = new Store(adatper); 21 | var type = "foo"; 22 | var id = "1"; 23 | var options = {}; 24 | var result; 25 | t.plan(6); 26 | t.doesNotThrow(function () { 27 | result = store.load(type, id, options); 28 | }, "should not throw an error"); 29 | t.equal(adatper.load.lastCall.args[0], store); 30 | t.equal(adatper.load.lastCall.args[1], type); 31 | t.equal(adatper.load.lastCall.args[2], id); 32 | t.equal(adatper.load.lastCall.args[3], options); 33 | t.equal(result, a); 34 | }); 35 | -------------------------------------------------------------------------------- /spec/store/clud/update-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import sinon from "sinon"; 3 | import Store from "../../../src/store"; 4 | 5 | test("update must throw an error if update is called when there isn't an adapter", function (t) { 6 | var store = new Store(); 7 | t.plan(1); 8 | t.throws(function () { 9 | store.update(); 10 | }, /Adapter missing\. Specify an adapter when creating the store: `var store = new Store\(adapter\);`/); 11 | }); 12 | 13 | test("update must call the update method provided by the adapter", function (t) { 14 | var a = {}; 15 | var adatper = { 16 | update: sinon.spy(function () { 17 | return a; 18 | }) 19 | }; 20 | var store = new Store(adatper); 21 | var type = "foo"; 22 | var id = "1"; 23 | var partial = {}; 24 | var options = {}; 25 | var result; 26 | t.plan(7); 27 | t.doesNotThrow(function () { 28 | result = store.update(type, id, partial, options); 29 | }, "should not throw an error"); 30 | t.equal(adatper.update.lastCall.args[0], store); 31 | t.equal(adatper.update.lastCall.args[1], type); 32 | t.equal(adatper.update.lastCall.args[2], id); 33 | t.equal(adatper.update.lastCall.args[3], partial); 34 | t.equal(adatper.update.lastCall.args[4], options); 35 | t.equal(result, a); 36 | }); 37 | -------------------------------------------------------------------------------- /spec/store/core/add-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import Store from "../../../src/store"; 3 | 4 | test("add must add a resource to the store", function (t) { 5 | var store = new Store(); 6 | t.plan(2); 7 | store.define("products", {}); 8 | store.add({ 9 | "type": "products", 10 | "id": "44" 11 | }); 12 | t.equal(store.find("products").length, 1); 13 | t.equal(store.find("products")[0].id, "44"); 14 | }); 15 | 16 | test("add must throw an error if the data doesn't have a type and id", function (t) { 17 | var store = new Store(); 18 | t.plan(1); 19 | t.throws(function () { 20 | store.add({}); 21 | }, /The data must have a type and id/); 22 | }); 23 | 24 | test("add must throw an error when called without arguments", function (t) { 25 | var store = new Store(); 26 | t.plan(1); 27 | t.throws(function () { 28 | store.add(); 29 | }, /You must provide data to add/); 30 | }); 31 | 32 | test("add must throw an error if the type has not been defined", function (t) { 33 | var store = new Store(); 34 | t.plan(1); 35 | t.throws(function () { 36 | store.add({ 37 | "type": "products", 38 | "id": "44" 39 | }); 40 | }, /Unknown type 'products'/); 41 | }); 42 | 43 | test("add must use deserialize functions provided by type definitions", function (t) { 44 | var store = new Store(); 45 | t.plan(1); 46 | store.define("products", { 47 | title: { 48 | deserialize: function (data, key) { 49 | return "Example " + data.id + " " + key; 50 | } 51 | } 52 | }); 53 | store.add({ 54 | "type": "products", 55 | "id": "44" 56 | }); 57 | t.equal(store.find("products", "44").title, "Example 44 title"); 58 | }); 59 | 60 | test("add must not set fields when the deserialize function returns undefined", function (t) { 61 | var store = new Store(); 62 | t.plan(2); 63 | store.define("products", { 64 | title: { 65 | deserialize: function (data) { 66 | if (data.attributes && data.attributes.title) { 67 | return "Example"; 68 | } 69 | } 70 | } 71 | }); 72 | store.add({ 73 | "type": "products", 74 | "id": "44", 75 | "attributes": { 76 | "title": true 77 | } 78 | }); 79 | t.equal(store.find("products", "44").title, "Example"); 80 | store.add({ 81 | "type": "products", 82 | "id": "44" 83 | }); 84 | t.equal(store.find("products", "44").title, "Example"); 85 | }); 86 | 87 | test("add must set fields when the deserialize function returns null", function (t) { 88 | var store = new Store(); 89 | t.plan(2); 90 | store.define("products", { 91 | title: { 92 | deserialize: function (data) { 93 | if (data.attributes && data.attributes.title) { 94 | return "Example"; 95 | } else { 96 | return null; 97 | } 98 | } 99 | } 100 | }); 101 | store.add({ 102 | "type": "products", 103 | "id": "44", 104 | "attributes": { 105 | "title": true 106 | } 107 | }); 108 | t.equal(store.find("products", "44").title, "Example"); 109 | store.add({ 110 | "type": "products", 111 | "id": "44" 112 | }); 113 | t.equal(store.find("products", "44").title, null); 114 | }); 115 | 116 | test("add must call deserialize functions in the context of the store", function (t) { 117 | var store = new Store(); 118 | t.plan(1); 119 | store.define("products", { 120 | example: { 121 | deserialize: function () { 122 | return this; 123 | } 124 | } 125 | }); 126 | store.add({ 127 | "type": "products", 128 | "id": "44" 129 | }); 130 | t.equal(store.find("products", "44").example, store); 131 | }); 132 | 133 | test("add inverse relationships must setup inverse one-to-one relationships", function (t) { 134 | var store = new Store(); 135 | t.plan(1); 136 | store.define("categories", { 137 | product: Store.hasOne({ inverse: "category" }) 138 | }); 139 | store.define("products", { 140 | category: Store.hasOne({ inverse: "product" }) 141 | }); 142 | store.add({ 143 | "type": "products", 144 | "id": "10", 145 | "relationships": { 146 | "category": { 147 | "data": { "type": "categories", "id": "1" } 148 | } 149 | } 150 | }); 151 | t.equal(store.find("categories", "1").product, store.find("products", "10")); 152 | }); 153 | 154 | test("add inverse relationships must setup inverse one-to-many relationships", function (t) { 155 | var store = new Store(); 156 | t.plan(1); 157 | store.define("categories", { 158 | products: Store.hasMany({ inverse: "category" }) 159 | }); 160 | store.define("products", { 161 | category: Store.hasOne({ inverse: "products" }) 162 | }); 163 | store.add({ 164 | "type": "products", 165 | "id": "10", 166 | "relationships": { 167 | "category": { 168 | "data": { "type": "categories", "id": "1" } 169 | } 170 | } 171 | }); 172 | store.add({ 173 | "type": "products", 174 | "id": "20", 175 | "relationships": { 176 | "category": { 177 | "data": { "type": "categories", "id": "1" } 178 | } 179 | } 180 | }); 181 | t.deepEqual(store.find("categories", "1").products.map(x => x.id).sort(), [ "10", "20" ]); 182 | }); 183 | 184 | test("add inverse relationships must setup inverse many-to-many relationships", function (t) { 185 | var store = new Store(); 186 | t.plan(1); 187 | store.define("categories", { 188 | products: Store.hasMany({ inverse: "categories" }) 189 | }); 190 | store.define("products", { 191 | categories: Store.hasMany({ inverse: "products" }) 192 | }); 193 | store.add({ 194 | "type": "categories", 195 | "id": "10", 196 | "relationships": { 197 | "products": { 198 | "data": [ 199 | { "type": "products", "id": "1" } 200 | ] 201 | } 202 | } 203 | }); 204 | store.add({ 205 | "type": "categories", 206 | "id": "20", 207 | "relationships": { 208 | "products": { 209 | "data": [ 210 | { "type": "products", "id": "1" } 211 | ] 212 | } 213 | } 214 | }); 215 | t.deepEqual(store.find("products", "1").categories.map(x => x.id).sort(), [ "10", "20" ]); 216 | }); 217 | 218 | test("add inverse relationships must setup inverse many-to-one relationships", function (t) { 219 | var store = new Store(); 220 | t.plan(1); 221 | store.define("categories", { 222 | products: Store.hasMany({ inverse: "category" }) 223 | }); 224 | store.define("products", { 225 | category: Store.hasOne({ inverse: "products" }) 226 | }); 227 | store.add({ 228 | "type": "categories", 229 | "id": "10", 230 | "relationships": { 231 | "products": { 232 | "data": [ 233 | { "type": "products", "id": "1" } 234 | ] 235 | } 236 | } 237 | }); 238 | store.add({ 239 | "type": "categories", 240 | "id": "20", 241 | "relationships": { 242 | "products": { 243 | "data": [ 244 | { "type": "products", "id": "1" } 245 | ] 246 | } 247 | } 248 | }); 249 | t.equal(store.find("products", "1").category, store.find("categories", "20")); 250 | }); 251 | 252 | test("add inverse relationships must throw an error when an inverse relationship is an attribute", function (t) { 253 | var store = new Store(); 254 | t.plan(2); 255 | store.define("categories", { 256 | product: Store.attr() 257 | }); 258 | store.define("comments", { 259 | product: Store.attr() 260 | }); 261 | store.define("products", { 262 | category: Store.hasOne({ inverse: "product" }), 263 | comments: Store.hasMany({ inverse: "product" }) 264 | }); 265 | t.throws(function () { 266 | store.add({ 267 | "type": "products", 268 | "id": "44", 269 | "relationships": { 270 | "category": { 271 | "data": { "type": "categories", "id": "34" } 272 | } 273 | } 274 | }); 275 | }, /The the inverse relationship for 'category' is an attribute \('product'\)/); 276 | t.throws(function () { 277 | store.add({ 278 | "type": "products", 279 | "id": "44", 280 | "relationships": { 281 | "comments": { 282 | "data": [ 283 | { "type": "comments", "id": "3" } 284 | ] 285 | } 286 | } 287 | }); 288 | }, /The the inverse relationship for 'comments' is an attribute \('product'\)/); 289 | }); 290 | 291 | test("add inverse relationships must throw an error when an explict inverse relationship is absent", function (t) { 292 | var store = new Store(); 293 | t.plan(3); 294 | store.define("categories", {}); 295 | store.define("comments", {}); 296 | store.define("products", { 297 | category: Store.hasOne({ inverse: "product" }), 298 | comments: Store.hasMany({ inverse: "products" }), 299 | users: Store.hasMany() 300 | }); 301 | store.define("users", {}); 302 | t.throws(function () { 303 | store.add({ 304 | "type": "products", 305 | "id": "44", 306 | "relationships": { 307 | "category": { 308 | "data": { "type": "categories", "id": "34" } 309 | } 310 | } 311 | }); 312 | }, /The the inverse relationship for 'category' is missing \('product'\)/); 313 | t.throws(function () { 314 | store.add({ 315 | "type": "products", 316 | "id": "44", 317 | "relationships": { 318 | "comments": { 319 | "data": [ 320 | { "type": "comments", "id": "3" } 321 | ] 322 | } 323 | } 324 | }); 325 | }, /The the inverse relationship for 'comments' is missing \('products'\)/); 326 | t.doesNotThrow(function () { 327 | store.add({ 328 | "type": "products", 329 | "id": "44", 330 | "relationships": { 331 | "users": { 332 | "data": [ 333 | { "type": "users", "id": "6" } 334 | ] 335 | } 336 | } 337 | }); 338 | }); 339 | }); 340 | 341 | test("add inverse relationships must not try to process inverse relationships for absent relationships", function (t) { 342 | var store = new Store(); 343 | t.plan(1); 344 | store.define("categories", { 345 | products: Store.hasMany({ inverse: "category" }) 346 | }); 347 | store.define("products", { 348 | category: Store.hasOne({ inverse: "products" }) 349 | }); 350 | t.doesNotThrow(function () { 351 | store.add({ 352 | "type": "products", 353 | "id": "44" 354 | }); 355 | }); 356 | }); 357 | 358 | test("add inverse relationships must remove null (has one) inverse relationships", function (t) { 359 | var store = new Store(); 360 | t.plan(2); 361 | store.define("categories", { 362 | products: Store.hasMany({ inverse: "category" }) 363 | }); 364 | store.define("products", { 365 | category: Store.hasOne({ inverse: "products" }) 366 | }); 367 | store.add({ 368 | "type": "products", 369 | "id": "44", 370 | "relationships": { 371 | "category": { 372 | "data": { "type": "categories", "id": "34" } 373 | } 374 | } 375 | }); 376 | t.deepEqual(store.find("categories", "34").products.map(x => x.id).sort(), [ "44" ]); 377 | store.add({ 378 | "type": "products", 379 | "id": "44", 380 | "relationships": { 381 | "category": { 382 | "data": null 383 | } 384 | } 385 | }); 386 | t.deepEqual(store.find("categories", "34").products, []); 387 | }); 388 | 389 | test("add inverse relationships must remove empty (has many) inverse relationships", function (t) { 390 | var store = new Store(); 391 | t.plan(3); 392 | store.define("categories", { 393 | products: Store.hasMany({ inverse: "category" }) 394 | }); 395 | store.define("products", { 396 | category: Store.hasOne({ inverse: "products" }) 397 | }); 398 | store.add({ 399 | "type": "categories", 400 | "id": "37", 401 | "relationships": { 402 | "products": { 403 | "data": [ 404 | { "type": "products", "id": "23" }, 405 | { "type": "products", "id": "45" } 406 | ] 407 | } 408 | } 409 | }); 410 | t.equal(store.find("products", "23").category, store.find("categories", "37")); 411 | t.equal(store.find("products", "45").category, store.find("categories", "37")); 412 | store.add({ 413 | "type": "categories", 414 | "id": "37", 415 | "relationships": { 416 | "products": { 417 | "data": [ 418 | { "type": "products", "id": "23" } 419 | ] 420 | } 421 | } 422 | }); 423 | t.equal(store.find("products", "45").category, null); 424 | }); 425 | 426 | test("add inverse relationships must use the type's name as a fallback for relationship names when adding resources", function (t) { 427 | var store = new Store(); 428 | t.plan(2); 429 | store.define("categories", { 430 | products: Store.hasMany() 431 | }); 432 | store.define("products", { 433 | categories: Store.hasMany() 434 | }); 435 | store.add({ 436 | "type": "categories", 437 | "id": "37", 438 | "relationships": { 439 | "products": { 440 | "data": [ 441 | { "type": "products", "id": "23" } 442 | ] 443 | } 444 | } 445 | }); 446 | t.deepEqual(store.find("categories", "37").products.map(x => x.id).sort(), [ "23" ]); 447 | t.deepEqual(store.find("products", "23").categories.map(x => x.id).sort(), [ "37" ]); 448 | }); 449 | -------------------------------------------------------------------------------- /spec/store/core/convert-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import sinon from "sinon"; 3 | import Store from "../../../src/store"; 4 | 5 | test("convert must use serialize functions provided by type definitions", function (t) { 6 | var store = new Store(); 7 | var serialize = sinon.spy(function (resource, data) { 8 | data.attributes.title = resource.title + "!"; 9 | }); 10 | var partial = { 11 | title: "Example" 12 | }; 13 | t.plan(3); 14 | store.define("products", { 15 | title: { 16 | serialize: serialize 17 | } 18 | }); 19 | t.deepEqual(store.convert("products", "44", partial), { type: "products", id: "44", attributes: { title: "Example!" }, relationships: {} }); 20 | t.equal(serialize.firstCall.args[0], partial); 21 | t.equal(serialize.firstCall.args[2], "title"); 22 | }); 23 | 24 | test("convert must automatically extract the type and id if they're not passed", function (t) { 25 | var store = new Store(); 26 | var partial = { 27 | type: "products", 28 | id: "44" 29 | }; 30 | t.plan(3); 31 | store.define("products", {}); 32 | t.doesNotThrow(function () { 33 | store.convert(partial); 34 | }); 35 | t.equal(store.convert(partial).type, "products"); 36 | t.equal(store.convert(partial).id, "44"); 37 | }); 38 | 39 | test("convert must not include the id if it wasn't provided", function (t) { 40 | var store = new Store(); 41 | t.plan(3); 42 | store.define("products", {}); 43 | t.equal(store.convert({ type: "products" }).id, undefined); 44 | t.equal(store.convert("products", {}).id, undefined); 45 | t.equal(store.convert("products", null, {}).id, undefined); 46 | }); 47 | -------------------------------------------------------------------------------- /spec/store/core/define-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import Store from "../../../src/store"; 3 | 4 | test("define must accept pseudonyms", function (t) { 5 | var store = new Store(); 6 | t.plan(4); 7 | store.define([ "comments", "comment" ], { 8 | product: Store.hasOne() 9 | }); 10 | store.define([ "products", "product" ], { 11 | comments: Store.hasMany() 12 | }); 13 | t.equal(store.find("comment", "67"), store.find("comments", "67")); 14 | t.equal(store.find("product", "8"), store.find("products", "8")); 15 | store.add({ 16 | "type": "comments", 17 | "id": "1", 18 | "relationships": { 19 | "product": { 20 | "data": { 21 | "type": "products", 22 | "id": "1" 23 | } 24 | } 25 | } 26 | }); 27 | t.equal(store.find("product", "1").comments[0], store.find("comment", "1")); 28 | t.equal(store.find("comments", "1").product, store.find("products", "1")); 29 | }); 30 | 31 | test("define must throw an error if you try to define a type that has already been defined", function (t) { 32 | var store = new Store(); 33 | t.plan(1); 34 | store.define("example", {}); 35 | t.throws(function () { 36 | store.define([ "sample", "example"], {}); 37 | }, /The type 'example' has already been defined\./); 38 | }); 39 | 40 | test("define must throw an error if you try to define a type that without providing a definition", function (t) { 41 | var store = new Store(); 42 | t.plan(1); 43 | t.throws(function () { 44 | store.define("example"); 45 | }, /You must provide a definition for the type 'example'\./); 46 | }); 47 | -------------------------------------------------------------------------------- /spec/store/core/find-all-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import Store from "../../../src/store"; 3 | 4 | test("find must return an array of objects", function (t) { 5 | var store = new Store(); 6 | t.plan(1); 7 | store.define("products", {}); 8 | store.add({ type: "products", id: "1" }); 9 | t.notEqual(store.findAll("products").indexOf(store.find("products", "1")), -1); 10 | }); 11 | 12 | test("find must throw an error when trying to find an unknown resource type", function (t) { 13 | var store = new Store(); 14 | t.plan(1); 15 | t.throws(function () { 16 | store.findAll("products"); 17 | }, /Unknown type 'products'/); 18 | }); 19 | 20 | test("find must throw an error when called without a type", function (t) { 21 | var store = new Store(); 22 | t.plan(1); 23 | t.throws(function () { 24 | store.find(); 25 | }, /You must provide a type/); 26 | }); 27 | -------------------------------------------------------------------------------- /spec/store/core/find-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import Store from "../../../src/store"; 3 | 4 | test("find must return an object with 'type' and 'id' properties", function (t) { 5 | var store = new Store(); 6 | t.plan(2); 7 | store.define("products", {}); 8 | t.equal(store.find("products", "23").type, "products"); 9 | t.equal(store.find("products", "74").id, "74"); 10 | }); 11 | 12 | test("find must return the same object if called with the same arguments", function (t) { 13 | var store = new Store(); 14 | t.plan(1); 15 | store.define("products", {}); 16 | t.equal(store.find("products", "23"), store.find("products", "23")); 17 | }); 18 | 19 | test("find must throw an error when trying to find an unknown resource type", function (t) { 20 | var store = new Store(); 21 | t.plan(1); 22 | t.throws(function () { 23 | store.find("products", "1"); 24 | }, /Unknown type 'products'/); 25 | }); 26 | 27 | test("find must throw an error when called without a type", function (t) { 28 | var store = new Store(); 29 | t.plan(1); 30 | t.throws(function () { 31 | store.find(); 32 | }, /You must provide a type/); 33 | }); 34 | 35 | // test("find must throw an error when called without an id", function (t) { 36 | // var store = new Store(); 37 | // store.define("products", {}); 38 | // t.plan(1); 39 | // t.throws(function () { 40 | // store.find("products"); 41 | // }, /You must provide an id/); 42 | // }); 43 | 44 | test("find must give fields their default values", function (t) { 45 | var store = new Store(); 46 | t.plan(1); 47 | store.define("products", { 48 | title: { 49 | default: "example" 50 | } 51 | }); 52 | t.equal(store.find("products", "1").title, "example"); 53 | }); 54 | -------------------------------------------------------------------------------- /spec/store/core/push-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import sinon from "sinon"; 3 | import Store from "../../../src/store"; 4 | 5 | test("push must add a single resource to the store", function (t) { 6 | var store = new Store(); 7 | t.plan(1); 8 | store.define("products", {}); 9 | sinon.spy(store, "add"); 10 | var root = { 11 | "data": { 12 | "type": "products", 13 | "id": "34" 14 | } 15 | }; 16 | store.push(root); 17 | t.ok(store.add.calledWith(root.data), "should call add with data pushed"); 18 | }); 19 | 20 | test("push must add a collection of resources to the store", function (t) { 21 | var store = new Store(); 22 | t.plan(2); 23 | store.define("products", {}); 24 | sinon.spy(store, "add"); 25 | var root = { 26 | "data": [ 27 | { 28 | "type": "products", 29 | "id": "34" 30 | }, 31 | { 32 | "type": "products", 33 | "id": "74" 34 | } 35 | ] 36 | }; 37 | store.push(root); 38 | t.ok(store.add.calledWith(root.data[0]), "should call add with data pushed"); 39 | t.ok(store.add.calledWith(root.data[1]), "should call add with data pushed"); 40 | }); 41 | 42 | test("push must add included resources to the store", function (t) { 43 | var store = new Store(); 44 | t.plan(2); 45 | store.define("categories", {}); 46 | store.define("products", {}); 47 | sinon.spy(store, "add"); 48 | var root = { 49 | "data": { 50 | "type": "categories", 51 | "id": "34" 52 | }, 53 | "included": [ 54 | { 55 | "type": "products", 56 | "id": "34" 57 | }, 58 | { 59 | "type": "products", 60 | "id": "74" 61 | } 62 | ] 63 | }; 64 | store.push(root); 65 | t.ok(store.add.calledWith(root.included[0]), "should call add with data pushed"); 66 | t.ok(store.add.calledWith(root.included[1]), "should call add with data pushed"); 67 | }); 68 | -------------------------------------------------------------------------------- /spec/store/core/remove-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import sinon from "sinon"; 3 | import Store from "../../../src/store"; 4 | 5 | test("remove must remove a resource from the store", function (t) { 6 | var store = new Store(); 7 | t.plan(2); 8 | store.define("products", {}); 9 | store.add({ 10 | "type": "products", 11 | "id": "44" 12 | }); 13 | t.equal(store.find("products").length, 1); 14 | store.remove("products", "44"); 15 | t.equal(store.find("products").length, 0); 16 | }); 17 | 18 | test("remove must remove call remove for each resources of a type if no id is given", function (t) { 19 | var store = new Store(); 20 | t.plan(4); 21 | store.define("products", {}); 22 | store.add({ 23 | "type": "products", 24 | "id": "44" 25 | }); 26 | store.add({ 27 | "type": "products", 28 | "id": "47" 29 | }); 30 | t.equal(store.find("products").length, 2); 31 | sinon.spy(store, "remove"); 32 | store.remove("products"); 33 | t.equal(store.find("products").length, 0); 34 | t.ok(store.remove.calledWith("products", "44")); 35 | t.ok(store.remove.calledWith("products", "47")); 36 | }); 37 | 38 | test("remove must throw an error when called without arguments", function (t) { 39 | var store = new Store(); 40 | t.plan(1); 41 | t.throws(function () { 42 | store.remove(); 43 | }, /You must provide a type to remove/); 44 | }); 45 | 46 | test("remove must throw an error if the type has not been defined", function (t) { 47 | var store = new Store(); 48 | t.plan(1); 49 | t.throws(function () { 50 | store.remove("products"); 51 | }, /Unknown type 'products'/); 52 | }); 53 | 54 | test("remove must not throw an error if the a resource doesn't exist", function (t) { 55 | var store = new Store(); 56 | t.plan(1); 57 | store.define("products", {}); 58 | t.doesNotThrow(function () { 59 | store.remove("products", "1"); 60 | }); 61 | }); 62 | 63 | test("remove must remove dependant relationships when a resource is removed", function (t) { 64 | var store = new Store(); 65 | t.plan(3); 66 | store.define("categories", { 67 | products: Store.hasMany({ inverse: "category" }) 68 | }); 69 | store.define("products", { 70 | category: Store.hasOne({ inverse: "products" }) 71 | }); 72 | store.add({ 73 | "type": "categories", 74 | "id": "1", 75 | "relationships": { 76 | "products": { 77 | "data": [ 78 | { "type": "products", "id": "10" }, 79 | { "type": "products", "id": "11" } 80 | ] 81 | } 82 | } 83 | }); 84 | t.deepEqual(store.find("categories", "1").products.map(x => x.id).sort(), [ "10", "11" ]); 85 | store.remove("products", "10"); 86 | t.deepEqual(store.find("categories", "1").products.map(x => x.id).sort(), [ "11" ]); 87 | store.remove("categories", "1"); 88 | t.equal(store.find("products", "11").category, null); 89 | }); 90 | -------------------------------------------------------------------------------- /spec/store/events/observable-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import sinon from "sinon"; 3 | import Store from "../../../src/store"; 4 | 5 | test("observable must fire an added event when a resource is added to the store", function (t) { 6 | var store = new Store(); 7 | var listener = sinon.spy(); 8 | t.plan(5); 9 | store.define("products", {}); 10 | store.observable.subscribe(listener); 11 | store.add({ 12 | "type": "products", 13 | "id": "1" 14 | }); 15 | t.equal(listener.callCount, 1); 16 | t.equal(listener.firstCall.args[0].name, "added"); 17 | t.equal(listener.firstCall.args[0].type, "products"); 18 | t.equal(listener.firstCall.args[0].id, "1"); 19 | t.equal(listener.firstCall.args[0].resource, store.find("products", "1")); 20 | }); 21 | 22 | test("observable must fire an updated event when a resource is update in the store", function (t) { 23 | var store = new Store(); 24 | var listener = sinon.spy(); 25 | t.plan(5); 26 | store.define("products", { 27 | title: Store.attr() 28 | }); 29 | store.add({ 30 | "type": "products", 31 | "id": "1", 32 | "attributes": { 33 | "title": "foo" 34 | } 35 | }); 36 | store.observable.subscribe(listener); 37 | store.add({ 38 | "type": "products", 39 | "id": "1", 40 | "attributes": { 41 | "title": "bar" 42 | } 43 | }); 44 | t.equal(listener.callCount, 1); 45 | t.equal(listener.firstCall.args[0].name, "updated"); 46 | t.equal(listener.firstCall.args[0].type, "products"); 47 | t.equal(listener.firstCall.args[0].id, "1"); 48 | t.equal(listener.firstCall.args[0].resource, store.find("products", "1")); 49 | }); 50 | 51 | test("observable must fire a removed event when a resource is removed from the store", function (t) { 52 | var store = new Store(); 53 | var listener = sinon.spy(); 54 | t.plan(5); 55 | store.define("products", { 56 | title: Store.attr() 57 | }); 58 | store.add({ 59 | "type": "products", 60 | "id": "1", 61 | "attributes": { 62 | "title": "foo" 63 | } 64 | }); 65 | store.observable.subscribe(listener); 66 | store.remove("products", "1"); 67 | t.equal(listener.callCount, 1); 68 | t.equal(listener.firstCall.args[0].name, "removed"); 69 | t.equal(listener.firstCall.args[0].type, "products"); 70 | t.equal(listener.firstCall.args[0].id, "1"); 71 | t.equal(listener.firstCall.args[0].resource, null); 72 | }); 73 | -------------------------------------------------------------------------------- /spec/store/events/off-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import sinon from "sinon"; 3 | import Store from "../../../src/store"; 4 | 5 | test("off must remove event handlers", function (t) { 6 | var store = new Store(); 7 | var listener = sinon.spy(); 8 | var context = {}; 9 | t.plan(2); 10 | store.define([ "products", "product" ], {}); 11 | store.on("added", "products", "1", listener, context); 12 | store.on("added", "products", listener, context); 13 | store.on("updated", "products", "1", listener, context); 14 | store.on("updated", "products", listener, context); 15 | store.on("removed", "products", "1", listener, context); 16 | store.on("removed", "products", listener, context); 17 | store.add({ 18 | "type": "products", 19 | "id": "1" 20 | }); 21 | store.add({ 22 | "type": "products", 23 | "id": "1" 24 | }); 25 | store.remove("products", "1"); 26 | t.equal(listener.callCount, 6); 27 | store.off("added", "products", "1", listener); 28 | store.off("added", "products", listener); 29 | store.off("updated", "products", "1", listener); 30 | store.off("updated", "products", listener); 31 | store.off("removed", "products", "1", listener); 32 | store.off("removed", "products", listener); 33 | store.add({ 34 | "type": "products", 35 | "id": "1" 36 | }); 37 | store.add({ 38 | "type": "products", 39 | "id": "1" 40 | }); 41 | store.remove("products", "1"); 42 | t.equal(listener.callCount, 6); 43 | }); 44 | 45 | test("off must throw an error when an unknown event is passed", function (t) { 46 | var store = new Store(); 47 | var listener = sinon.spy(); 48 | t.plan(1); 49 | store.define([ "products", "product" ], {}); 50 | t.throws(function () { 51 | store.off("foo", "products", "1", listener); 52 | }, /Unknown event 'foo'/); 53 | }); 54 | 55 | test("off must throw an error if the type has not been defined", function (t) { 56 | var store = new Store(); 57 | var listener = sinon.spy(); 58 | t.plan(1); 59 | store.define([ "products", "product" ], {}); 60 | t.throws(function () { 61 | store.off("added", "foo", "1", listener); 62 | }, /Unknown type 'foo'/); 63 | }); 64 | 65 | test("off must remove listeners added with a different pseudonym", function (t) { 66 | var store = new Store(); 67 | var listener = sinon.spy(); 68 | t.plan(1); 69 | store.define([ "products", "product" ], {}); 70 | store.on("added", "products", listener); 71 | store.off("added", "product", listener); 72 | store.add({ 73 | "type": "products", 74 | "id": "1" 75 | }); 76 | t.equal(listener.callCount, 0); 77 | }); 78 | -------------------------------------------------------------------------------- /spec/store/events/on-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import sinon from "sinon"; 3 | import Store from "../../../src/store"; 4 | 5 | test("on must fire an added event when the resource with the given type & id is added to the store", function (t) { 6 | var store = new Store(); 7 | var listener = sinon.spy(); 8 | var context = {}; 9 | t.plan(2); 10 | store.define([ "products", "product" ], {}); 11 | store.on("added", "products", "1", listener, context); 12 | store.add({ 13 | "type": "products", 14 | "id": "1" 15 | }); 16 | t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); 17 | t.ok(listener.calledOn(context), "should be called on context given"); 18 | }); 19 | 20 | test("on must fire an added event when the resource with the given type is added to the store", function (t) { 21 | var store = new Store(); 22 | var listener = sinon.spy(); 23 | var context = {}; 24 | t.plan(2); 25 | store.define([ "products", "product" ], {}); 26 | store.on("added", "products", listener, context); 27 | store.add({ 28 | "type": "products", 29 | "id": "1" 30 | }); 31 | t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); 32 | t.ok(listener.calledOn(context), "should be called on context given"); 33 | }); 34 | 35 | test("on must fire an updated event when the resource with the given type & id is updated in the store", function (t) { 36 | var store = new Store(); 37 | var listener = sinon.spy(); 38 | var context = {}; 39 | t.plan(3); 40 | store.define([ "products", "product" ], {}); 41 | store.on("updated", "products", "1", listener, context); 42 | store.add({ 43 | "type": "products", 44 | "id": "1" 45 | }); 46 | t.equal(listener.callCount, 0); 47 | store.add({ 48 | "type": "products", 49 | "id": "1" 50 | }); 51 | t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); 52 | t.ok(listener.calledOn(context), "should be called on context given"); 53 | }); 54 | 55 | test("on must fire an updated event when the resource with the given type is updated in the store", function (t) { 56 | var store = new Store(); 57 | var listener = sinon.spy(); 58 | var context = {}; 59 | t.plan(3); 60 | store.define([ "products", "product" ], {}); 61 | store.on("updated", "products", listener, context); 62 | store.add({ 63 | "type": "products", 64 | "id": "1" 65 | }); 66 | t.equal(listener.callCount, 0); 67 | store.add({ 68 | "type": "products", 69 | "id": "1" 70 | }); 71 | t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); 72 | t.ok(listener.calledOn(context), "should be called on context given"); 73 | }); 74 | 75 | test("on must fire an removed event when the resource with the given type & id is removed from the store", function (t) { 76 | var store = new Store(); 77 | var listener = sinon.spy(); 78 | var context = {}; 79 | t.plan(3); 80 | store.define([ "products", "product" ], {}); 81 | store.on("removed", "products", "1", listener, context); 82 | store.add({ 83 | "type": "products", 84 | "id": "1" 85 | }); 86 | t.equal(listener.callCount, 0); 87 | store.remove("products", "1"); 88 | t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); 89 | t.ok(listener.calledOn(context), "should be called on context given"); 90 | }); 91 | 92 | test("on must fire an removed event when the resource with the given type is removed from the store", function (t) { 93 | var store = new Store(); 94 | var listener = sinon.spy(); 95 | var context = {}; 96 | t.plan(3); 97 | store.define([ "products", "product" ], {}); 98 | store.on("removed", "products", listener, context); 99 | store.add({ 100 | "type": "products", 101 | "id": "1" 102 | }); 103 | t.equal(listener.callCount, 0); 104 | store.remove("products", "1"); 105 | t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); 106 | t.ok(listener.calledOn(context), "should be called on context given"); 107 | }); 108 | 109 | test("on must not fire an added event when automatically creating a resource as the result of a find", function (t) { 110 | var store = new Store(); 111 | var listener = sinon.spy(); 112 | var context = {}; 113 | t.plan(1); 114 | store.define([ "products", "product" ], {}); 115 | store.on("added", "products", "1", listener, context); 116 | store.find("products", "1"); 117 | t.equal(listener.callCount, 0); 118 | }); 119 | 120 | test("on must throw an error when an unknown event is passed", function (t) { 121 | var store = new Store(); 122 | var listener = sinon.spy(); 123 | var context = {}; 124 | t.plan(1); 125 | store.define([ "products", "product" ], {}); 126 | t.throws(function () { 127 | store.on("foo", "products", "1", listener, context); 128 | }, /Unknown event 'foo'/); 129 | }); 130 | 131 | test("on must throw an error if the type has not been defined", function (t) { 132 | var store = new Store(); 133 | var listener = sinon.spy(); 134 | var context = {}; 135 | t.plan(1); 136 | store.define([ "products", "product" ], {}); 137 | t.throws(function () { 138 | store.on("added", "foo", "1", listener, context); 139 | }, /Unknown type 'foo'/); 140 | }); 141 | 142 | test("on must call listeners that were added using a different pseudonym", function (t) { 143 | var store = new Store(); 144 | var listener = sinon.spy(); 145 | var context = {}; 146 | t.plan(2); 147 | store.define([ "products", "product" ], {}); 148 | store.on("added", "product", listener, context); 149 | store.add({ 150 | "type": "products", 151 | "id": "1" 152 | }); 153 | t.equal(listener.callCount, 1); 154 | t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); 155 | }); 156 | 157 | test("on must not add the same listener multiple times for the same type", function (t) { 158 | var store = new Store(); 159 | var listener = sinon.spy(); 160 | var context = {}; 161 | t.plan(2); 162 | store.define([ "products", "product" ], {}); 163 | store.on("added", "products", listener, context); 164 | store.on("added", "products", listener); 165 | store.add({ 166 | "type": "products", 167 | "id": "1" 168 | }); 169 | t.equal(listener.callCount, 1); 170 | t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); 171 | }); 172 | 173 | test("on must not add the same listener multiple times for the same type and id", function (t) { 174 | var store = new Store(); 175 | var listener = sinon.spy(); 176 | var context = {}; 177 | t.plan(2); 178 | store.define([ "products", "product" ], {}); 179 | store.on("added", "products", "1", listener, context); 180 | store.on("added", "products", "1", listener); 181 | store.add({ 182 | "type": "products", 183 | "id": "1" 184 | }); 185 | t.equal(listener.callCount, 1); 186 | t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); 187 | }); 188 | 189 | test("on must add the same listener multiple times if the id is different", function (t) { 190 | var store = new Store(); 191 | var listener = sinon.spy(); 192 | var context = {}; 193 | t.plan(3); 194 | store.define([ "products", "product" ], {}); 195 | store.on("added", "products", "1", listener, context); 196 | store.on("added", "products", "2", listener, context); 197 | store.add({ 198 | "type": "products", 199 | "id": "1" 200 | }); 201 | store.add({ 202 | "type": "products", 203 | "id": "2" 204 | }); 205 | t.equal(listener.callCount, 2); 206 | t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); 207 | t.ok(listener.calledWith(store.find("products", "2")), "called with resource"); 208 | }); 209 | 210 | test("on must not add the same listener multiple times for different pseudonyms of the same type", function (t) { 211 | var store = new Store(); 212 | var listener = sinon.spy(); 213 | var context = {}; 214 | t.plan(2); 215 | store.define([ "products", "product" ], {}); 216 | store.on("added", "products", listener, context); 217 | store.on("added", "product", listener, context); 218 | store.add({ 219 | "type": "products", 220 | "id": "1" 221 | }); 222 | t.equal(listener.callCount, 1); 223 | t.ok(listener.calledWith(store.find("products", "1")), "called with resource"); 224 | }); 225 | -------------------------------------------------------------------------------- /spec/store/fields/attr-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import Store from "../../../src/store"; 3 | 4 | test("attr must return the correct type attribute", function (t) { 5 | t.plan(1); 6 | t.equal(Store.attr().type, "attr"); 7 | }); 8 | 9 | test("attr must return a definition that has the default option passed", function (t) { 10 | t.plan(2); 11 | t.equal(Store.attr({ default: "foo" }).default, "foo"); 12 | t.equal(Store.attr("example", { default: "foo" }).default, "foo"); 13 | }); 14 | 15 | test("attr must return a deserialize function that maps to the attribute provided", function (t) { 16 | var field = Store.attr("example-title"); 17 | var data = { 18 | "type": "products", 19 | "id": "1", 20 | "attributes": { 21 | "example-title": "Example" 22 | } 23 | }; 24 | t.plan(1); 25 | t.equal(field.deserialize.call(this, data), "Example"); 26 | }); 27 | 28 | test("attr must return a deserialize function that maps to the key if no attribute name is provided", function (t) { 29 | var store = new Store(); 30 | var field = Store.attr(); 31 | var data = { 32 | "type": "products", 33 | "id": "1", 34 | "attributes": { 35 | "title": "Example" 36 | } 37 | }; 38 | t.plan(1); 39 | t.equal(field.deserialize.call(store, data, "title"), "Example"); 40 | }); 41 | 42 | test("attr must return a serialize function that maps to the attribute provided", function (t) { 43 | var field = Store.attr("example-title"); 44 | var resource = { 45 | "type": "products", 46 | "id": "1", 47 | "example-title": "Example" 48 | }; 49 | var data = { 50 | type: "products", 51 | id: "1", 52 | attributes: {}, 53 | relationships: {} 54 | }; 55 | t.plan(1); 56 | field.serialize.call(this, resource, data, "example-title"); 57 | t.equal(data.attributes["example-title"], "Example"); 58 | }); 59 | 60 | test("attr must return a serialize function that maps to the key if no attribute name is provided", function (t) { 61 | var store = new Store(); 62 | var field = Store.attr(); 63 | var resource = { 64 | "type": "products", 65 | "id": "1", 66 | "title": "Example" 67 | }; 68 | var data = { 69 | type: "products", 70 | id: "1", 71 | attributes: {}, 72 | relationships: {} 73 | }; 74 | t.plan(1); 75 | field.serialize.call(store, resource, data, "title"); 76 | t.equal(data.attributes["title"], "Example"); 77 | }); 78 | 79 | test("attr must return undefined when the attribute is missing from the data", function (t) { 80 | var store = new Store(); 81 | var field = Store.attr("title"); 82 | t.plan(2); 83 | t.equal(field.deserialize.call(store, { 84 | "type": "products", 85 | "id": "1", 86 | "attributes": {} 87 | }, "title"), undefined); 88 | t.equal(field.deserialize.call(store, { 89 | "type": "products", 90 | "id": "1" 91 | }, "title"), undefined); 92 | }); 93 | -------------------------------------------------------------------------------- /spec/store/fields/has-many-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import Store from "../../../src/store"; 3 | 4 | test("hasMany must return the correct type attribute", function (t) { 5 | t.plan(4); 6 | t.equal(Store.hasMany().type, "has-many"); 7 | t.equal(Store.hasMany("example").type, "has-many"); 8 | t.equal(Store.hasMany({}).type, "has-many"); 9 | t.equal(Store.hasMany("example", {}).type, "has-many"); 10 | }); 11 | 12 | test("hasMany must return default value as an empty array", function (t) { 13 | t.plan(1); 14 | t.deepEqual(Store.hasMany().default, []); 15 | }); 16 | 17 | test("hasMany must return a serialize function that maps a relationship to data", function (t) { 18 | var store = new Store(); 19 | var field = Store.hasMany(); 20 | var resource = { 21 | type: "products", 22 | id: "1", 23 | categories: [ 24 | { 25 | type: "categories", 26 | id: "2" 27 | }, 28 | { 29 | type: "categories", 30 | id: "4" 31 | } 32 | ] 33 | }; 34 | var data = { 35 | "relationships": {} 36 | }; 37 | t.plan(1); 38 | store.define("categories", {}); 39 | store.define("products", {}); 40 | field.serialize.call(store, resource, data, "categories"); 41 | t.deepEqual(data, { 42 | relationships: { 43 | categories: { 44 | data: [ 45 | { 46 | type: "categories", 47 | id: "2" 48 | }, 49 | { 50 | type: "categories", 51 | id: "4" 52 | } 53 | ] 54 | } 55 | } 56 | }); 57 | }); 58 | 59 | test("hasMany must return a serialize function that skips missing relationships", function (t) { 60 | var store = new Store(); 61 | var field = Store.hasMany(); 62 | var resource = { 63 | type: "products", 64 | id: "1" 65 | }; 66 | var data = { 67 | "relationships": {} 68 | }; 69 | t.plan(1); 70 | store.define("categories", {}); 71 | store.define("products", {}); 72 | field.serialize.call(store, resource, data, "categories"); 73 | t.deepEqual(data, { 74 | relationships: {} 75 | }); 76 | }); 77 | 78 | // TODO: serialize function uses the name option if it's provided 79 | 80 | test("hasMany must return a serialize function that uses the name option if it's provided", function (t) { 81 | var store = new Store(); 82 | var field = Store.hasMany("categories-x"); 83 | var resource = { 84 | type: "products", 85 | id: "1", 86 | categories: [ 87 | { 88 | type: "categories", 89 | id: "2" 90 | }, 91 | { 92 | type: "categories", 93 | id: "4" 94 | } 95 | ] 96 | }; 97 | var data = { 98 | "relationships": {} 99 | }; 100 | t.plan(1); 101 | store.define("categories", {}); 102 | store.define("products", {}); 103 | field.serialize.call(store, resource, data, "categories"); 104 | t.deepEqual(data, { 105 | relationships: { 106 | "categories-x": { 107 | data: [ 108 | { 109 | type: "categories", 110 | id: "2" 111 | }, 112 | { 113 | type: "categories", 114 | id: "4" 115 | } 116 | ] 117 | } 118 | } 119 | }); 120 | }); 121 | 122 | test("hasMany must return a deserialize function that maps to the relation described in the data property", function (t) { 123 | var store = new Store(); 124 | var field = Store.hasMany("categories"); 125 | var data = { 126 | "type": "products", 127 | "id": "1", 128 | "relationships": { 129 | "categories": { 130 | "data": [ 131 | { 132 | "type": "categories", 133 | "id": "2" 134 | }, 135 | { 136 | "type": "categories", 137 | "id": "4" 138 | } 139 | ] 140 | } 141 | } 142 | }; 143 | t.plan(2); 144 | store.define("categories", {}); 145 | store.define("products", {}); 146 | t.equal(field.deserialize.call(store, data).length, 2); 147 | t.deepEqual(field.deserialize.call(store, data).map(c => c.id).sort(), [ "2", "4" ]); 148 | }); 149 | 150 | test("hasMany must return a deserialize function that uses the key param when the name isn't provided", function (t) { 151 | var store = new Store(); 152 | var field = Store.hasMany(); 153 | var data = { 154 | "type": "products", 155 | "id": "1", 156 | "relationships": { 157 | "categories": { 158 | "data": [ 159 | { 160 | "type": "categories", 161 | "id": "2" 162 | }, 163 | { 164 | "type": "categories", 165 | "id": "4" 166 | } 167 | ] 168 | } 169 | } 170 | }; 171 | t.plan(2); 172 | store.define("categories", {}); 173 | store.define("products", {}); 174 | t.equal(field.deserialize.call(store, data, "categories").length, 2); 175 | t.deepEqual(field.deserialize.call(store, data, "categories").map(c => c.id).sort(), [ "2", "4" ]); 176 | }); 177 | 178 | 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) { 179 | var store = new Store(); 180 | var data = { 181 | "type": "products", 182 | "id": "1", 183 | "relationships": { 184 | "categories": { 185 | "data": [] 186 | }, 187 | "comments": { 188 | "data": null 189 | } 190 | } 191 | }; 192 | t.plan(4); 193 | t.deepEqual(Store.hasMany("categories").deserialize.call(store, data), []); 194 | t.deepEqual(Store.hasMany().deserialize.call(store, data, "categories"), []); 195 | t.deepEqual(Store.hasMany("comments").deserialize.call(store, data), []); 196 | t.deepEqual(Store.hasMany().deserialize.call(store, data, "comments"), []); 197 | }); 198 | 199 | test("hasMany must return a deserialize function that returns undefined when the relationship data field is missing", function (t) { 200 | var store = new Store(); 201 | var data = { 202 | "type": "products", 203 | "id": "1", 204 | "relationships": { 205 | "categories": {} 206 | } 207 | }; 208 | t.plan(2); 209 | t.equal(Store.hasMany("categories").deserialize.call(store, data), undefined); 210 | t.equal(Store.hasMany().deserialize.call(store, data, "categories"), undefined); 211 | }); 212 | 213 | test("hasMany must return a deserialize function that returns undefined when the relationship type field is missing", function (t) { 214 | var store = new Store(); 215 | var data = { 216 | "type": "products", 217 | "id": "1", 218 | "relationships": {} 219 | }; 220 | t.plan(2); 221 | t.equal(Store.hasMany("categories").deserialize.call(store, data), undefined); 222 | t.equal(Store.hasMany().deserialize.call(store, data, "categories"), undefined); 223 | }); 224 | 225 | test("hasMany must return a deserialize function that returns undefined when the relationship field is missing", function (t) { 226 | var store = new Store(); 227 | var data = { 228 | "type": "products", 229 | "id": "1" 230 | }; 231 | t.plan(2); 232 | t.equal(Store.hasMany("categories").deserialize.call(store, data), undefined); 233 | t.equal(Store.hasMany().deserialize.call(store, data, "categories"), undefined); 234 | }); 235 | 236 | test("hasMany must return a deserialize function that passes on an inverse option", function (t) { 237 | t.plan(2); 238 | t.equal(Store.hasMany({ inverse: "foo" }).inverse, "foo"); 239 | t.equal(Store.hasMany("example", { inverse: "foo" }).inverse, "foo"); 240 | }); 241 | 242 | test("hasMany must throw an error a relationship's type hasn't been defined", function (t) { 243 | var store = new Store(); 244 | var field = Store.hasMany(); 245 | var data = { 246 | "type": "products", 247 | "id": "44", 248 | "relationships": { 249 | "categories": { 250 | "data": [ 251 | { "type": "categories", "id": "34" } 252 | ] 253 | } 254 | } 255 | }; 256 | t.plan(1); 257 | store.define("products", {}); 258 | t.throws(function () { 259 | field.deserialize.call(store, data, "categories"); 260 | }, /Unknown type 'categories'/); 261 | }); 262 | -------------------------------------------------------------------------------- /spec/store/fields/has-one-spec.js: -------------------------------------------------------------------------------- 1 | import test from "tape-catch"; 2 | import Store from "../../../src/store"; 3 | 4 | test("hasOne must return the correct type attribute", function (t) { 5 | t.plan(4); 6 | t.equal(Store.hasOne().type, "has-one"); 7 | t.equal(Store.hasOne("example").type, "has-one"); 8 | t.equal(Store.hasOne({}).type, "has-one"); 9 | t.equal(Store.hasOne("example", {}).type, "has-one"); 10 | }); 11 | 12 | test("hasOne must return a serialize function that maps a relationship to data", function (t) { 13 | var store = new Store(); 14 | var field = Store.hasOne(); 15 | var resource = { 16 | type: "products", 17 | id: "1", 18 | category: { 19 | type: "categories", 20 | id: "2" 21 | } 22 | }; 23 | var data = { 24 | "relationships": {} 25 | }; 26 | t.plan(1); 27 | store.define("categories", {}); 28 | store.define("products", {}); 29 | field.serialize.call(store, resource, data, "category"); 30 | t.deepEqual(data, { 31 | relationships: { 32 | category: { 33 | data: { 34 | type: "categories", 35 | id: "2" 36 | } 37 | } 38 | } 39 | }); 40 | }); 41 | 42 | test("hasOne must return a serialize function that skips missing relationships", function (t) { 43 | var store = new Store(); 44 | var field = Store.hasOne(); 45 | var resource = { 46 | type: "products", 47 | id: "1" 48 | }; 49 | var data = { 50 | "relationships": {} 51 | }; 52 | t.plan(1); 53 | store.define("categories", {}); 54 | store.define("products", {}); 55 | field.serialize.call(store, resource, data, "category"); 56 | t.deepEqual(data, { 57 | relationships: {} 58 | }); 59 | }); 60 | 61 | test("hasOne must return a serialize function that sets null relationships as null", function (t) { 62 | var store = new Store(); 63 | var field = Store.hasOne(); 64 | var resource = { 65 | type: "products", 66 | id: "1", 67 | category: null 68 | }; 69 | var data = { 70 | "relationships": {} 71 | }; 72 | t.plan(1); 73 | store.define("categories", {}); 74 | store.define("products", {}); 75 | field.serialize.call(store, resource, data, "category"); 76 | t.deepEqual(data, { 77 | relationships: { 78 | category: null 79 | } 80 | }); 81 | }); 82 | 83 | test("hasOne must return a serialize function that uses the name option if it's provided", function (t) { 84 | var store = new Store(); 85 | var field = Store.hasOne("category-x"); 86 | var resource = { 87 | type: "products", 88 | id: "1", 89 | category: { 90 | type: "categories", 91 | id: "2" 92 | }, 93 | foo: null 94 | }; 95 | var data = { 96 | "relationships": {} 97 | }; 98 | t.plan(2); 99 | store.define("categories", {}); 100 | store.define("products", {}); 101 | field.serialize.call(store, resource, data, "category"); 102 | t.deepEqual(data, { 103 | relationships: { 104 | "category-x": { 105 | data: { 106 | type: "categories", 107 | id: "2" 108 | } 109 | } 110 | } 111 | }); 112 | data = { 113 | "relationships": {} 114 | }; 115 | field.serialize.call(store, resource, data, "foo"); 116 | t.deepEqual(data, { 117 | relationships: { 118 | "category-x": null 119 | } 120 | }); 121 | }); 122 | 123 | test("hasOne must return a deserialize function that maps to the relation described in the data property", function (t) { 124 | var store = new Store(); 125 | var field = Store.hasOne("category"); 126 | var data = { 127 | "type": "products", 128 | "id": "1", 129 | "relationships": { 130 | "category": { 131 | "data": { 132 | "type": "categories", 133 | "id": "2" 134 | } 135 | } 136 | } 137 | }; 138 | t.plan(1); 139 | store.define("categories", {}); 140 | store.define("products", {}); 141 | t.equal(field.deserialize.call(store, data), store.find("categories", "2")); 142 | }); 143 | 144 | test("hasOne must return a deserialize function that uses the key param when the name isn't provided", function (t) { 145 | var store = new Store(); 146 | var field = Store.hasOne(); 147 | var data = { 148 | "type": "products", 149 | "id": "1", 150 | "relationships": { 151 | "category": { 152 | "data": { 153 | "type": "categories", 154 | "id": "2" 155 | } 156 | } 157 | } 158 | }; 159 | t.plan(1); 160 | store.define("categories", {}); 161 | store.define("products", {}); 162 | t.equal(field.deserialize.call(store, data, "category"), store.find("categories", "2")); 163 | }); 164 | 165 | test("hasOne must return a deserialize function that returns null when the relationship data field is null", function (t) { 166 | var store = new Store(); 167 | var data = { 168 | "type": "products", 169 | "id": "1", 170 | "relationships": { 171 | "category": { 172 | "data": null 173 | } 174 | } 175 | }; 176 | t.plan(2); 177 | t.equal(Store.hasOne("category").deserialize.call(store, data), null); 178 | t.equal(Store.hasOne().deserialize.call(store, data, "category"), null); 179 | }); 180 | 181 | test("hasOne must return a deserialize function that returns undefined when the relationship data field is missing", function (t) { 182 | var store = new Store(); 183 | var data = { 184 | "type": "products", 185 | "id": "1", 186 | "relationships": { 187 | "category": {} 188 | } 189 | }; 190 | t.plan(2); 191 | t.equal(Store.hasOne("category").deserialize.call(store, data), undefined); 192 | t.equal(Store.hasOne().deserialize.call(store, data, "category"), undefined); 193 | }); 194 | 195 | test("hasOne must return a deserialize function that returns undefined when the relationship type field is missing", function (t) { 196 | var store = new Store(); 197 | var data = { 198 | "type": "products", 199 | "id": "1", 200 | "relationships": {} 201 | }; 202 | t.plan(2); 203 | t.equal(Store.hasOne("category").deserialize.call(store, data), undefined); 204 | t.equal(Store.hasOne().deserialize.call(store, data, "category"), undefined); 205 | }); 206 | 207 | test("hasOne must return a deserialize function that returns undefined when the relationship field is missing", function (t) { 208 | var store = new Store(); 209 | var data = { 210 | "type": "products", 211 | "id": "1" 212 | }; 213 | t.plan(2); 214 | t.equal(Store.hasOne("category").deserialize.call(store, data), undefined); 215 | t.equal(Store.hasOne().deserialize.call(store, data, "category"), undefined); 216 | }); 217 | 218 | test("hasOne must return a deserialize function that passes on an inverse option", function (t) { 219 | t.plan(2); 220 | t.equal(Store.hasOne({ inverse: "foo" }).inverse, "foo"); 221 | t.equal(Store.hasOne("example", { inverse: "foo" }).inverse, "foo"); 222 | }); 223 | 224 | test("hasOne must throw an error a relationship's type hasn't been defined", function (t) { 225 | var store = new Store(); 226 | var field = Store.hasOne(); 227 | var data = { 228 | "type": "products", 229 | "id": "44", 230 | "relationships": { 231 | "category": { 232 | "data": { "type": "categories", "id": "34" } 233 | } 234 | } 235 | }; 236 | t.plan(1); 237 | store.define("products", {}); 238 | t.throws(function () { 239 | field.deserialize.call(store, data, "category"); 240 | }, /Unknown type 'categories'/); 241 | }); 242 | -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "ecmaFeatures": { 6 | "arrowFunctions": true, 7 | "blockBindings": true, 8 | "classes": true, 9 | "modules": true, 10 | "templateStrings": true 11 | }, 12 | "rules": { 13 | "block-scoped-var": 2, 14 | "complexity": 2, 15 | "consistent-return": 2, 16 | "curly": 2, 17 | "default-case": 2, 18 | "eqeqeq": 2, 19 | "guard-for-in": 2, 20 | "no-alert": 2, 21 | "no-caller": 2, 22 | "no-console": 1, 23 | "no-div-regex": 2, 24 | "no-else-return": 0, 25 | "no-empty-label": 2, 26 | "no-eq-null": 2, 27 | "no-eval": 2, 28 | "no-extend-native": 2, 29 | "no-extra-bind": 2, 30 | "no-fallthrough": 2, 31 | "no-floating-decimal": 2, 32 | "no-implied-eval": 2, 33 | "no-iterator": 2, 34 | "no-labels": 2, 35 | "no-lone-blocks": 2, 36 | "no-loop-func": 2, 37 | "no-multi-spaces": 2, 38 | "no-multi-str": 2, 39 | "no-native-reassign": 2, 40 | "no-new": 2, 41 | "no-new-func": 2, 42 | "no-new-wrappers": 2, 43 | "no-octal": 2, 44 | "no-octal-escape": 2, 45 | "no-process-env": 2, 46 | "no-proto": 2, 47 | "no-redeclare": 2, 48 | "no-return-assign": 2, 49 | "no-script-url": 2, 50 | "no-self-compare": 2, 51 | "no-sequences": 2, 52 | "no-undef": 2, 53 | "no-unused-expressions": 2, 54 | "no-unused-vars": 2, 55 | "no-void": 2, 56 | "no-warning-comments": 1, 57 | "no-with": 2, 58 | "radix": 2, 59 | "semi": 2, 60 | "vars-on-top": 2, 61 | "wrap-iife": 2, 62 | "yoda": 2 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/ajax-adapter.js: -------------------------------------------------------------------------------- 1 | import ajax from "./ajax"; 2 | 3 | export default class AjaxAdapter { 4 | 5 | constructor(options) { 6 | this._base = (options && options.base) || ""; 7 | }; 8 | 9 | create(store, type, partial, options) { 10 | 11 | if (!store._types[type]) { 12 | throw new Error(`Unknown type '${type}'`); 13 | } 14 | 15 | let source = ajax({ 16 | body: JSON.stringify({ 17 | data: store.convert(type, partial) 18 | }), 19 | crossDomain: true, 20 | headers: { 21 | "Content-Type": "application/vnd.api+json" 22 | }, 23 | method: "POST", 24 | responseType: "auto", 25 | url: this._getUrl(type, null, options) 26 | }).do(e => store.push(e.response)) 27 | .map(e => store.find(e.response.data.type, e.response.data.id)) 28 | .publish(); 29 | 30 | source.connect(); 31 | 32 | return source; 33 | 34 | } 35 | 36 | destroy(store, type, id, options) { 37 | 38 | if (!store._types[type]) { 39 | throw new Error(`Unknown type '${type}'`); 40 | } 41 | 42 | let source = ajax({ 43 | crossDomain: true, 44 | headers: { 45 | "Content-Type": "application/vnd.api+json" 46 | }, 47 | method: "DELETE", 48 | responseType: "auto", 49 | url: this._getUrl(type, id, options) 50 | }).do(() => store.remove(type, id)) 51 | .publish(); 52 | 53 | source.connect(); 54 | 55 | return source; 56 | 57 | } 58 | 59 | load(store, type, id, options) { 60 | 61 | if (id && typeof id === "object") { 62 | return this.load(store, type, null, id); 63 | } 64 | 65 | if (!store._types[type]) { 66 | throw new Error(`Unknown type '${type}'`); 67 | } 68 | 69 | let source = ajax({ 70 | crossDomain: true, 71 | headers: { 72 | "Content-Type": "application/vnd.api+json" 73 | }, 74 | method: "GET", 75 | responseType: "auto", 76 | url: this._getUrl(type, id, options) 77 | }).do(e => store.push(e.response)) 78 | .map(() => id ? store.find(type, id) : store.findAll(type)) 79 | .publish(); 80 | 81 | source.connect(); 82 | 83 | return source; 84 | 85 | } 86 | 87 | update(store, type, id, partial, options) { 88 | 89 | if (!store._types[type]) { 90 | throw new Error(`Unknown type '${type}'`); 91 | } 92 | 93 | let data = store.convert(type, id, partial); 94 | 95 | let source = ajax({ 96 | body: JSON.stringify({ 97 | data: data 98 | }), 99 | crossDomain: true, 100 | headers: { 101 | "Content-Type": "application/vnd.api+json" 102 | }, 103 | method: "PATCH", 104 | responseType: "auto", 105 | url: this._getUrl(type, id, options) 106 | }).do(() => store.add(data)) 107 | .map(() => store.find(type, id)) 108 | .publish(); 109 | 110 | source.connect(); 111 | 112 | return source; 113 | 114 | } 115 | 116 | _getUrl(type, id, options) { 117 | 118 | let params = []; 119 | let url = id ? `${this._base}/${type}/${id}` : `${this._base}/${type}`; 120 | 121 | if (options) { 122 | 123 | if (options.fields) { 124 | Object.keys(options.fields).forEach(field => { 125 | options[`fields[${field}]`] = options.fields[field]; 126 | }); 127 | delete options.fields; 128 | } 129 | 130 | params = Object.keys(options).map(key => { 131 | return key + "=" + encodeURIComponent(options[key]); 132 | }).sort(); 133 | 134 | if (params.length) { 135 | url = `${url}?${params.join("&")}`; 136 | } 137 | 138 | } 139 | 140 | return url; 141 | 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/ajax.js: -------------------------------------------------------------------------------- 1 | import Rx from "rx"; 2 | 3 | /** 4 | * 5 | * Derived from RxJS-DOM, Copyright (c) Microsoft Open Technologies, Inc: 6 | * 7 | * https://github.com/Reactive-Extensions/RxJS-DOM 8 | * 9 | * The original source file can be viewed here: 10 | * 11 | * https://github.com/Reactive-Extensions/RxJS-DOM/blob/fdb169c8bd1612318d530e6e54074b1c9e537906/src/ajax.js 12 | * 13 | * Licensed under the Apache License, Version 2.0: 14 | * 15 | * http://www.apache.org/licenses/LICENSE-2.0 16 | * 17 | * Modifications from original: 18 | * 19 | * - extracted from "Rx.DOM" namespace 20 | * - minor eslinter cleanup ("var" to "let" etc) 21 | * - addition of "auto" responseType 22 | * 23 | */ 24 | 25 | var root = (typeof window !== "undefined" && window) || this; 26 | 27 | // Gets the proper XMLHttpRequest for support for older IE 28 | function getXMLHttpRequest() { 29 | if (root.XMLHttpRequest) { 30 | return new root.XMLHttpRequest(); 31 | } else { 32 | let progId; 33 | try { 34 | let progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0']; 35 | for(let i = 0; i < 3; i++) { 36 | try { 37 | progId = progIds[i]; 38 | if (new root.ActiveXObject(progId)) { 39 | break; 40 | } 41 | } catch(e) { } 42 | } 43 | return new root.ActiveXObject(progId); 44 | } catch (e) { 45 | throw new Error('XMLHttpRequest is not supported by your browser'); 46 | } 47 | } 48 | } 49 | 50 | // Get CORS support even for older IE 51 | function getCORSRequest() { 52 | var xhr = new root.XMLHttpRequest(); 53 | if ('withCredentials' in xhr) { 54 | return xhr; 55 | } else if (!!root.XDomainRequest) { 56 | return new XDomainRequest(); 57 | } else { 58 | throw new Error('CORS is not supported by your browser'); 59 | } 60 | } 61 | 62 | function normalizeAjaxSuccessEvent(e, xhr, settings) { 63 | var response = ('response' in xhr) ? xhr.response : xhr.responseText; 64 | if (settings.responseType === 'auto') { 65 | try { 66 | response = JSON.parse(response); 67 | } catch (e) {} 68 | } else { 69 | response = settings.responseType === 'json' ? JSON.parse(response) : response; 70 | } 71 | return { 72 | response: response, 73 | status: xhr.status, 74 | responseType: xhr.responseType, 75 | xhr: xhr, 76 | originalEvent: e 77 | }; 78 | } 79 | 80 | function normalizeAjaxErrorEvent(e, xhr, type) { 81 | return { 82 | type: type, 83 | status: xhr.status, 84 | xhr: xhr, 85 | originalEvent: e 86 | }; 87 | } 88 | 89 | /** 90 | * Creates an observable for an Ajax request with either a settings object with url, headers, etc or a string for a URL. 91 | * 92 | * @example 93 | * source = ajax('/products'); 94 | * source = ajax({ url: 'products', method: 'GET' }); 95 | * 96 | * @param {Object} settings Can be one of the following: 97 | * 98 | * A string of the URL to make the Ajax call. 99 | * An object with the following properties 100 | * - url: URL of the request 101 | * - body: The body of the request 102 | * - method: Method of the request, such as GET, POST, PUT, PATCH, DELETE 103 | * - async: Whether the request is async 104 | * - headers: Optional headers 105 | * - crossDomain: true if a cross domain request, else false 106 | * - responseType: "text" (default), "json" or "auto" 107 | * 108 | * @returns {Observable} An observable sequence containing the XMLHttpRequest. 109 | */ 110 | export default function (options) { 111 | var settings = { 112 | method: 'GET', 113 | crossDomain: false, 114 | async: true, 115 | headers: {}, 116 | responseType: 'text', 117 | createXHR: function(){ 118 | return this.crossDomain ? getCORSRequest() : getXMLHttpRequest(); 119 | }, 120 | normalizeError: normalizeAjaxErrorEvent, 121 | normalizeSuccess: normalizeAjaxSuccessEvent 122 | }; 123 | 124 | if(typeof options === 'string') { 125 | settings.url = options; 126 | } else { 127 | for(let prop in options) { 128 | if(hasOwnProperty.call(options, prop)) { 129 | settings[prop] = options[prop]; 130 | } 131 | } 132 | } 133 | 134 | let normalizeError = settings.normalizeError; 135 | let normalizeSuccess = settings.normalizeSuccess; 136 | 137 | if (!settings.crossDomain && !settings.headers['X-Requested-With']) { 138 | settings.headers['X-Requested-With'] = 'XMLHttpRequest'; 139 | } 140 | settings.hasContent = settings.body !== undefined; 141 | 142 | return new Rx.AnonymousObservable(function (observer) { 143 | var isDone = false; 144 | var xhr; 145 | 146 | var processResponse = function(xhr, e){ 147 | var status = xhr.status === 1223 ? 204 : xhr.status; 148 | if ((status >= 200 && status <= 300) || status === 0 || status === '') { 149 | observer.onNext(normalizeSuccess(e, xhr, settings)); 150 | observer.onCompleted(); 151 | } else { 152 | observer.onError(normalizeError(e, xhr, 'error')); 153 | } 154 | isDone = true; 155 | }; 156 | 157 | try { 158 | xhr = settings.createXHR();; 159 | } catch (err) { 160 | observer.onError(err); 161 | } 162 | 163 | try { 164 | if (settings.user) { 165 | xhr.open(settings.method, settings.url, settings.async, settings.user, settings.password); 166 | } else { 167 | xhr.open(settings.method, settings.url, settings.async); 168 | } 169 | 170 | let headers = settings.headers; 171 | for (let header in headers) { 172 | if (hasOwnProperty.call(headers, header)) { 173 | xhr.setRequestHeader(header, headers[header]); 174 | } 175 | } 176 | 177 | if(!!xhr.upload || (!('withCredentials' in xhr) && !!root.XDomainRequest)) { 178 | xhr.onload = function(e) { 179 | if(settings.progressObserver) { 180 | settings.progressObserver.onNext(e); 181 | settings.progressObserver.onCompleted(); 182 | } 183 | processResponse(xhr, e); 184 | }; 185 | 186 | if(settings.progressObserver) { 187 | xhr.onprogress = function(e) { 188 | settings.progressObserver.onNext(e); 189 | }; 190 | } 191 | 192 | xhr.onerror = function(e) { 193 | if (settings.progressObserver) { 194 | settings.progressObserver.onError(e); 195 | } 196 | observer.onError(normalizeError(e, xhr, 'error')); 197 | isDone = true; 198 | }; 199 | 200 | xhr.onabort = function(e) { 201 | if (settings.progressObserver) { 202 | settings.progressObserver.onError(e); 203 | } 204 | observer.onError(normalizeError(e, xhr, 'abort')); 205 | isDone = true; 206 | }; 207 | } else { 208 | 209 | xhr.onreadystatechange = function (e) { 210 | if (xhr.readyState === 4) { 211 | processResponse(xhr, e); 212 | } 213 | }; 214 | } 215 | 216 | xhr.send(settings.hasContent && settings.body || null); 217 | } catch (e) { 218 | observer.onError(e); 219 | } 220 | 221 | return function () { 222 | if (!isDone && xhr.readyState !== 4) { xhr.abort(); } 223 | }; 224 | }); 225 | }; 226 | --------------------------------------------------------------------------------