├── .codeclimate.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── NetworkStore.d.ts ├── NetworkStore.js ├── NetworkUtils.d.ts ├── NetworkUtils.js ├── Record.d.ts ├── Record.js ├── Response.d.ts ├── Response.js ├── Store.d.ts ├── Store.js ├── enums │ ├── ParamArrayType.d.ts │ └── ParamArrayType.js ├── index.d.ts ├── index.js ├── interfaces │ ├── ICache.d.ts │ ├── ICache.js │ ├── IDictionary.d.ts │ ├── IDictionary.js │ ├── IFilters.d.ts │ ├── IFilters.js │ ├── IHeaders.d.ts │ ├── IHeaders.js │ ├── IJsonApiIdentifier.d.ts │ ├── IJsonApiIdentifier.js │ ├── IJsonApiRecord.d.ts │ ├── IJsonApiRecord.js │ ├── IJsonApiRelationship.d.ts │ ├── IJsonApiRelationship.js │ ├── IJsonApiResponse.d.ts │ ├── IJsonApiResponse.js │ ├── IRawResponse.d.ts │ ├── IRawResponse.js │ ├── IRequestOptions.d.ts │ ├── IRequestOptions.js │ ├── IResponseHeaders.d.ts │ ├── IResponseHeaders.js │ ├── JsonApi.d.ts │ └── JsonApi.js ├── utils.d.ts └── utils.js ├── jsonapi.md ├── package-lock.json ├── package.json ├── src ├── NetworkStore.ts ├── NetworkUtils.ts ├── Record.ts ├── Response.ts ├── Store.ts ├── enums │ └── ParamArrayType.ts ├── index.ts ├── interfaces │ ├── ICache.ts │ ├── IDictionary.ts │ ├── IFilters.ts │ ├── IHeaders.ts │ ├── IRawResponse.ts │ ├── IRequestOptions.ts │ ├── IResponseHeaders.ts │ └── JsonApi.ts └── utils.ts ├── test ├── general.ts ├── issues.ts ├── main.ts ├── mocha.opts ├── mock │ ├── error.json │ ├── event-1.json │ ├── event-1b.json │ ├── event-1c.json │ ├── event-1d.json │ ├── events-1.json │ ├── events-2.json │ ├── image-1.json │ ├── invalid.json │ ├── issue-29.json │ ├── issue-84a.json │ ├── issue-84b.json │ ├── issue-84c.json │ ├── issue-84d.json │ ├── issue-84e.json │ ├── issue-falsy-meta-value.json │ ├── jsonapi-object.json │ ├── queue-1.json │ └── session-1.json ├── network │ ├── basics.ts │ ├── caching.ts │ ├── error-handling.ts │ ├── headers.ts │ ├── params.ts │ └── updates.ts └── utils │ ├── api.ts │ └── setup.ts ├── tsconfig.json ├── tslint.json ├── typings.json └── typings ├── globals └── isomorphic-fetch │ ├── index.d.ts │ └── typings.json └── index.d.ts /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | duplication: 3 | enabled: true 4 | config: 5 | languages: 6 | - javascript 7 | fixme: 8 | enabled: true 9 | ratings: 10 | paths: 11 | "**.ts" 12 | exclude_paths: 13 | - dist/ 14 | - test/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | *.map 40 | dist/test.* 41 | 42 | .DS_Store 43 | 44 | .vscode -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | .codeclimate.yml 4 | .travis.yml 5 | tsconfig.json 6 | tslint.json 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "8" 5 | - "10" 6 | install: 7 | - npm install 8 | script: 9 | - npm test 10 | after_success: 11 | - npm install -g codeclimate-test-reporter 12 | - codeclimate-test-reporter < coverage/lcov.info 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Infinum 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 | # mobx-jsonapi-store 2 | 3 | JSON API Store for MobX 4 | 5 | ## Deprecation and migration 6 | 7 | `mobx-jsonapi-store` and `mobx-collection-store` are deprecated in favor of [`datx`](https://github.com/infinum/datx) - it follows the same concepts, but adds support for MobX 4 and 5, better TypeScript support and more extensibility. 8 | 9 | If you're new to the libraries, check out the datx [examples](https://github.com/infinum/datx/tree/master/examples) and [docs](https://github.com/infinum/datx/wiki). 10 | 11 | If you already use `mobx-jsonapi-store`, check out the [migration guide](https://github.com/infinum/datx/wiki/Migration-from-mobx-jsonapi-store). 12 | 13 | ------- 14 | 15 | Don't need any [JSON API](http://jsonapi.org/) specific features? Check out [mobx-collection-store](https://github.com/infinum/mobx-collection-store). 16 | 17 | **Can be used with [Redux DevTools](https://github.com/infinum/mobx-jsonapi-store/wiki/Redux-DevTools).** 18 | 19 | [![Build Status](https://travis-ci.org/infinum/mobx-jsonapi-store.svg?branch=master)](https://travis-ci.org/infinum/mobx-jsonapi-store) 20 | [![Test Coverage](https://codeclimate.com/github/infinum/mobx-jsonapi-store/badges/coverage.svg)](https://codeclimate.com/github/infinum/mobx-jsonapi-store/coverage) 21 | [![npm version](https://badge.fury.io/js/mobx-jsonapi-store.svg)](https://badge.fury.io/js/mobx-jsonapi-store) 22 | 23 | [![Dependency Status](https://david-dm.org/infinum/mobx-jsonapi-store.svg)](https://david-dm.org/infinum/mobx-jsonapi-store) 24 | [![devDependency Status](https://david-dm.org/infinum/mobx-jsonapi-store/dev-status.svg)](https://david-dm.org/infinum/mobx-jsonapi-store#info=devDependencies) 25 | [![Greenkeeper badge](https://badges.greenkeeper.io/infinum/mobx-jsonapi-store.svg)](https://greenkeeper.io/) 26 | 27 | ## Basic example 28 | 29 | ```javascript 30 | import {Store} from 'mobx-jsonapi-store'; 31 | 32 | const store = new Store(); 33 | const user = store.sync(userResponse); // Assumption: userResponse was received from some API call and it's a valid JSON API response 34 | console.log(user.name); // "John" 35 | ``` 36 | 37 | For more, check out the [Getting started](https://github.com/infinum/mobx-jsonapi-store/wiki/Getting-started) guide. 38 | 39 | ## Installation 40 | 41 | To install, use `npm` or `yarn`. The lib has a peer dependency of `mobx` 2.7.0 or later (including MobX 3) and `mobx-collection-store`. 42 | 43 | ```bash 44 | npm install mobx-jsonapi-store mobx-collection-store mobx --save 45 | ``` 46 | 47 | ```bash 48 | yarn add mobx-jsonapi-store mobx-collection-store mobx 49 | ``` 50 | 51 | Since the lib is exposed as a set of CommonJS modules, you'll need something like [webpack](https://webpack.js.org/) or browserify in order to use it in the browser. 52 | 53 | Don't forget to [prepare your code for production](https://webpack.js.org/guides/production/) for better performance! 54 | 55 | # Migration from v3 to v4 56 | 57 | Version 4 has a few breaking changes described in the [migration guide](https://github.com/infinum/mobx-jsonapi-store/wiki/Migrating-from-v3-to-v4). 58 | 59 | # Getting started 60 | The main idea behind the library is to have one instance of the store that contains multiple model types. This way, there can be references between models that can handle all use cases, including circular dependencies. 61 | 62 | * [Setting up networking](https://github.com/infinum/mobx-jsonapi-store/wiki/Networking) 63 | * [Defining models](https://github.com/infinum/mobx-jsonapi-store/wiki/Defining-models) 64 | * [References](https://github.com/infinum/mobx-jsonapi-store/wiki/References) 65 | * [Configuring the store](https://github.com/infinum/mobx-jsonapi-store/wiki/Configuring-the-store) 66 | * [Using the store](https://github.com/infinum/mobx-jsonapi-store/wiki/Using-the-store) 67 | * [Using the network methods](https://github.com/infinum/mobx-jsonapi-store/wiki/Using-the-network) 68 | * [Persisting data locally](https://github.com/infinum/mobx-jsonapi-store/wiki/Persisting-data-locally) 69 | * [Redux DevTools](https://github.com/infinum/mobx-jsonapi-store/wiki/Redux-DevTools) 70 | 71 | # JSON API Spec 72 | mobx-jsonapi-store is [100% compatible with the JSON API v1.0 spec](https://github.com/infinum/mobx-jsonapi-store/wiki/JSON-API-Spec) 73 | 74 | # API reference 75 | 76 | * [config](https://github.com/infinum/mobx-jsonapi-store/wiki/config) 77 | * [Store](https://github.com/infinum/mobx-jsonapi-store/wiki/Store) 78 | * [Record](https://github.com/infinum/mobx-jsonapi-store/wiki/Record) 79 | * [Response](https://github.com/infinum/mobx-jsonapi-store/wiki/Response) 80 | * [TypeScript interfaces](https://github.com/infinum/mobx-jsonapi-store/wiki/Interfaces) 81 | 82 | ## License 83 | 84 | The [MIT License](LICENSE) 85 | 86 | ## Credits 87 | 88 | mobx-jsonapi-store is maintained and sponsored by 89 | [Infinum](http://www.infinum.co). 90 | 91 | 92 | -------------------------------------------------------------------------------- /dist/NetworkStore.d.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from 'mobx-collection-store'; 2 | import IHeaders from './interfaces/IHeaders'; 3 | import IRequestOptions from './interfaces/IRequestOptions'; 4 | import * as JsonApi from './interfaces/JsonApi'; 5 | export declare class NetworkStore extends Collection { 6 | /** 7 | * Prepare the query params for the API call 8 | * 9 | * @protected 10 | * @param {string} type Record type 11 | * @param {(number|string)} [id] Record ID 12 | * @param {JsonApi.IRequest} [data] Request data 13 | * @param {IRequestOptions} [options] Server options 14 | * @returns {{ 15 | * url: string, 16 | * data?: object, 17 | * headers: IHeaders, 18 | * }} Options needed for an API call 19 | * 20 | * @memberOf NetworkStore 21 | */ 22 | protected __prepareQuery(type: string, id?: number | string, data?: JsonApi.IRequest, options?: IRequestOptions): { 23 | url: string; 24 | data?: object; 25 | headers: IHeaders; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /dist/NetworkStore.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | extendStatics(d, b); 11 | function __() { this.constructor = d; } 12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 13 | }; 14 | })(); 15 | Object.defineProperty(exports, "__esModule", { value: true }); 16 | var mobx_collection_store_1 = require("mobx-collection-store"); 17 | var NetworkUtils_1 = require("./NetworkUtils"); 18 | var NetworkStore = /** @class */ (function (_super) { 19 | __extends(NetworkStore, _super); 20 | function NetworkStore() { 21 | return _super !== null && _super.apply(this, arguments) || this; 22 | } 23 | /** 24 | * Prepare the query params for the API call 25 | * 26 | * @protected 27 | * @param {string} type Record type 28 | * @param {(number|string)} [id] Record ID 29 | * @param {JsonApi.IRequest} [data] Request data 30 | * @param {IRequestOptions} [options] Server options 31 | * @returns {{ 32 | * url: string, 33 | * data?: object, 34 | * headers: IHeaders, 35 | * }} Options needed for an API call 36 | * 37 | * @memberOf NetworkStore 38 | */ 39 | NetworkStore.prototype.__prepareQuery = function (type, id, data, options) { 40 | var model = this.static.types.filter(function (item) { return item.type === type; })[0]; 41 | var headers = (options ? options.headers : {}) || {}; 42 | var url = NetworkUtils_1.buildUrl(type, id, model, options); 43 | return { data: data, headers: headers, url: url }; 44 | }; 45 | return NetworkStore; 46 | }(mobx_collection_store_1.Collection)); 47 | exports.NetworkStore = NetworkStore; 48 | -------------------------------------------------------------------------------- /dist/NetworkUtils.d.ts: -------------------------------------------------------------------------------- 1 | import { IModelConstructor } from 'mobx-collection-store'; 2 | import ParamArrayType from './enums/ParamArrayType'; 3 | import IDictionary from './interfaces/IDictionary'; 4 | import IHeaders from './interfaces/IHeaders'; 5 | import IRawResponse from './interfaces/IRawResponse'; 6 | import IRequestOptions from './interfaces/IRequestOptions'; 7 | import * as JsonApi from './interfaces/JsonApi'; 8 | import { Record } from './Record'; 9 | import { Response as LibResponse } from './Response'; 10 | import { Store } from './Store'; 11 | export declare type FetchType = (method: string, url: string, body?: object, requestHeaders?: IHeaders) => Promise; 12 | export interface IStoreFetchOpts { 13 | url: string; 14 | options?: IRequestOptions; 15 | data?: object; 16 | method: string; 17 | store: Store; 18 | } 19 | export declare type StoreFetchType = (options: IStoreFetchOpts) => Promise; 20 | export interface IConfigType { 21 | baseFetch: FetchType; 22 | baseUrl: string; 23 | defaultHeaders: IHeaders; 24 | defaultFetchOptions: IDictionary; 25 | fetchReference: Function; 26 | paramArrayType: ParamArrayType; 27 | storeFetch: StoreFetchType; 28 | transformRequest: (options: IStoreFetchOpts) => IStoreFetchOpts; 29 | transformResponse: (response: IRawResponse) => IRawResponse; 30 | } 31 | export declare const config: IConfigType; 32 | export declare function fetch(options: IStoreFetchOpts): Promise; 33 | /** 34 | * API call used to get data from the server 35 | * 36 | * @export 37 | * @param {Store} store Related Store 38 | * @param {string} url API call URL 39 | * @param {IHeaders} [headers] Headers to be sent 40 | * @param {IRequestOptions} [options] Server options 41 | * @returns {Promise} Resolves with a Response object 42 | */ 43 | export declare function read(store: Store, url: string, headers?: IHeaders, options?: IRequestOptions): Promise; 44 | /** 45 | * API call used to create data on the server 46 | * 47 | * @export 48 | * @param {Store} store Related Store 49 | * @param {string} url API call URL 50 | * @param {object} [data] Request body 51 | * @param {IHeaders} [headers] Headers to be sent 52 | * @param {IRequestOptions} [options] Server options 53 | * @returns {Promise} Resolves with a Response object 54 | */ 55 | export declare function create(store: Store, url: string, data?: object, headers?: IHeaders, options?: IRequestOptions): Promise; 56 | /** 57 | * API call used to update data on the server 58 | * 59 | * @export 60 | * @param {Store} store Related Store 61 | * @param {string} url API call URL 62 | * @param {object} [data] Request body 63 | * @param {IHeaders} [headers] Headers to be sent 64 | * @param {IRequestOptions} [options] Server options 65 | * @returns {Promise} Resolves with a Response object 66 | */ 67 | export declare function update(store: Store, url: string, data?: object, headers?: IHeaders, options?: IRequestOptions): Promise; 68 | /** 69 | * API call used to remove data from the server 70 | * 71 | * @export 72 | * @param {Store} store Related Store 73 | * @param {string} url API call URL 74 | * @param {IHeaders} [headers] Headers to be sent 75 | * @param {IRequestOptions} [options] Server options 76 | * @returns {Promise} Resolves with a Response object 77 | */ 78 | export declare function remove(store: Store, url: string, headers?: IHeaders, options?: IRequestOptions): Promise; 79 | /** 80 | * Fetch a link from the server 81 | * 82 | * @export 83 | * @param {JsonApi.ILink} link Link URL or a link object 84 | * @param {Store} store Store that will be used to save the response 85 | * @param {IDictionary} [requestHeaders] Request headers 86 | * @param {IRequestOptions} [options] Server options 87 | * @returns {Promise} Response promise 88 | */ 89 | export declare function fetchLink(link: JsonApi.ILink, store: Store, requestHeaders?: IDictionary, options?: IRequestOptions): Promise; 90 | export declare function handleResponse(record: Record, prop?: string): (response: LibResponse) => Record; 91 | export declare function prefixUrl(url: any): string; 92 | export declare function buildUrl(type: number | string, id?: number | string, model?: IModelConstructor, options?: IRequestOptions): string; 93 | -------------------------------------------------------------------------------- /dist/NetworkUtils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || function () { 3 | __assign = Object.assign || function(t) { 4 | for (var s, i = 1, n = arguments.length; i < n; i++) { 5 | s = arguments[i]; 6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 7 | t[p] = s[p]; 8 | } 9 | return t; 10 | }; 11 | return __assign.apply(this, arguments); 12 | }; 13 | Object.defineProperty(exports, "__esModule", { value: true }); 14 | var ParamArrayType_1 = require("./enums/ParamArrayType"); 15 | var Response_1 = require("./Response"); 16 | var utils_1 = require("./utils"); 17 | exports.config = { 18 | /** Base URL for all API calls */ 19 | baseUrl: '/', 20 | /** Default headers that will be sent to the server */ 21 | defaultHeaders: { 22 | 'content-type': 'application/vnd.api+json', 23 | }, 24 | /* Default options that will be passed to fetchReference */ 25 | defaultFetchOptions: {}, 26 | /** Reference of the fetch method that should be used */ 27 | /* istanbul ignore next */ 28 | fetchReference: utils_1.isBrowser && window.fetch && window.fetch.bind(window), 29 | /** Determines how will the request param arrays be stringified */ 30 | paramArrayType: ParamArrayType_1.default.COMMA_SEPARATED, 31 | /** 32 | * Base implementation of the fetch function (can be overridden) 33 | * 34 | * @param {string} method API call method 35 | * @param {string} url API call URL 36 | * @param {object} [body] API call body 37 | * @param {IHeaders} [requestHeaders] Headers that will be sent 38 | * @returns {Promise} Resolves with a raw response object 39 | */ 40 | baseFetch: function (method, url, body, requestHeaders) { 41 | var _this = this; 42 | var data; 43 | var status; 44 | var headers; 45 | var request = Promise.resolve(); 46 | var uppercaseMethod = method.toUpperCase(); 47 | var isBodySupported = uppercaseMethod !== 'GET' && uppercaseMethod !== 'HEAD'; 48 | return request 49 | .then(function () { 50 | var reqHeaders = utils_1.assign({}, exports.config.defaultHeaders, requestHeaders); 51 | var options = utils_1.assign({}, exports.config.defaultFetchOptions, { 52 | body: isBodySupported && JSON.stringify(body) || undefined, 53 | headers: reqHeaders, 54 | method: method, 55 | }); 56 | return _this.fetchReference(url, options); 57 | }) 58 | .then(function (response) { 59 | status = response.status; 60 | headers = response.headers; 61 | return response.json(); 62 | }) 63 | .catch(function (e) { 64 | if (status === 204) { 65 | return null; 66 | } 67 | throw e; 68 | }) 69 | .then(function (responseData) { 70 | data = responseData; 71 | if (status >= 400) { 72 | throw { 73 | message: "Invalid HTTP status: " + status, 74 | status: status, 75 | }; 76 | } 77 | return { data: data, headers: headers, requestHeaders: requestHeaders, status: status }; 78 | }) 79 | .catch(function (error) { 80 | return { data: data, error: error, headers: headers, requestHeaders: requestHeaders, status: status }; 81 | }); 82 | }, 83 | /** 84 | * Base implementation of the stateful fetch function (can be overridden) 85 | * 86 | * @param {IStoreFetchOpts} reqOptions API request options 87 | * @returns {Promise} Resolves with a response object 88 | */ 89 | storeFetch: function (reqOptions) { 90 | var _a = exports.config.transformRequest(reqOptions), url = _a.url, options = _a.options, data = _a.data, _b = _a.method, method = _b === void 0 ? 'GET' : _b, store = _a.store; 91 | return exports.config.baseFetch(method, url, data, options && options.headers) 92 | .then(function (response) { 93 | var storeResponse = utils_1.assign(response, { store: store }); 94 | return new Response_1.Response(exports.config.transformResponse(storeResponse), store, options); 95 | }); 96 | }, 97 | transformRequest: function (options) { 98 | return options; 99 | }, 100 | transformResponse: function (response) { 101 | return response; 102 | }, 103 | }; 104 | function fetch(options) { 105 | return exports.config.storeFetch(options); 106 | } 107 | exports.fetch = fetch; 108 | /** 109 | * API call used to get data from the server 110 | * 111 | * @export 112 | * @param {Store} store Related Store 113 | * @param {string} url API call URL 114 | * @param {IHeaders} [headers] Headers to be sent 115 | * @param {IRequestOptions} [options] Server options 116 | * @returns {Promise} Resolves with a Response object 117 | */ 118 | function read(store, url, headers, options) { 119 | return exports.config.storeFetch({ 120 | data: null, 121 | method: 'GET', 122 | options: __assign({}, options, { headers: headers }), 123 | store: store, 124 | url: url, 125 | }); 126 | } 127 | exports.read = read; 128 | /** 129 | * API call used to create data on the server 130 | * 131 | * @export 132 | * @param {Store} store Related Store 133 | * @param {string} url API call URL 134 | * @param {object} [data] Request body 135 | * @param {IHeaders} [headers] Headers to be sent 136 | * @param {IRequestOptions} [options] Server options 137 | * @returns {Promise} Resolves with a Response object 138 | */ 139 | function create(store, url, data, headers, options) { 140 | return exports.config.storeFetch({ 141 | data: data, 142 | method: 'POST', 143 | options: __assign({}, options, { headers: headers }), 144 | store: store, 145 | url: url, 146 | }); 147 | } 148 | exports.create = create; 149 | /** 150 | * API call used to update data on the server 151 | * 152 | * @export 153 | * @param {Store} store Related Store 154 | * @param {string} url API call URL 155 | * @param {object} [data] Request body 156 | * @param {IHeaders} [headers] Headers to be sent 157 | * @param {IRequestOptions} [options] Server options 158 | * @returns {Promise} Resolves with a Response object 159 | */ 160 | function update(store, url, data, headers, options) { 161 | return exports.config.storeFetch({ 162 | data: data, 163 | method: 'PATCH', 164 | options: __assign({}, options, { headers: headers }), 165 | store: store, 166 | url: url, 167 | }); 168 | } 169 | exports.update = update; 170 | /** 171 | * API call used to remove data from the server 172 | * 173 | * @export 174 | * @param {Store} store Related Store 175 | * @param {string} url API call URL 176 | * @param {IHeaders} [headers] Headers to be sent 177 | * @param {IRequestOptions} [options] Server options 178 | * @returns {Promise} Resolves with a Response object 179 | */ 180 | function remove(store, url, headers, options) { 181 | return exports.config.storeFetch({ 182 | data: null, 183 | method: 'DELETE', 184 | options: __assign({}, options, { headers: headers }), 185 | store: store, 186 | url: url, 187 | }); 188 | } 189 | exports.remove = remove; 190 | /** 191 | * Fetch a link from the server 192 | * 193 | * @export 194 | * @param {JsonApi.ILink} link Link URL or a link object 195 | * @param {Store} store Store that will be used to save the response 196 | * @param {IDictionary} [requestHeaders] Request headers 197 | * @param {IRequestOptions} [options] Server options 198 | * @returns {Promise} Response promise 199 | */ 200 | function fetchLink(link, store, requestHeaders, options) { 201 | if (link) { 202 | var href = typeof link === 'object' ? link.href : link; 203 | /* istanbul ignore else */ 204 | if (href) { 205 | return read(store, href, requestHeaders, options); 206 | } 207 | } 208 | return Promise.resolve(new Response_1.Response({ data: null }, store)); 209 | } 210 | exports.fetchLink = fetchLink; 211 | function handleResponse(record, prop) { 212 | return function (response) { 213 | /* istanbul ignore if */ 214 | if (response.error) { 215 | throw response.error; 216 | } 217 | if (response.status === 204) { 218 | record['__persisted'] = true; 219 | return record; 220 | } 221 | else if (response.status === 202) { 222 | response.data.update({ 223 | __prop__: prop, 224 | __queue__: true, 225 | __related__: record, 226 | }); 227 | return response.data; 228 | } 229 | else { 230 | record['__persisted'] = true; 231 | return response.replaceData(record).data; 232 | } 233 | }; 234 | } 235 | exports.handleResponse = handleResponse; 236 | function __prepareFilters(filters) { 237 | return __parametrize(filters).map(function (item) { return "filter[" + item.key + "]=" + item.value; }); 238 | } 239 | function __prepareSort(sort) { 240 | return sort ? ["sort=" + sort] : []; 241 | } 242 | function __prepareIncludes(include) { 243 | return include ? ["include=" + include] : []; 244 | } 245 | function __prepareFields(fields) { 246 | var list = []; 247 | utils_1.objectForEach(fields, function (key) { 248 | list.push("fields[" + key + "]=" + fields[key]); 249 | }); 250 | return list; 251 | } 252 | function __prepareRawParams(params) { 253 | return params.map(function (param) { 254 | if (typeof param === 'string') { 255 | return param; 256 | } 257 | return param.key + "=" + param.value; 258 | }); 259 | } 260 | function prefixUrl(url) { 261 | return "" + exports.config.baseUrl + url; 262 | } 263 | exports.prefixUrl = prefixUrl; 264 | function __appendParams(url, params) { 265 | if (params.length) { 266 | url += '?' + params.join('&'); 267 | } 268 | return url; 269 | } 270 | function __parametrize(params, scope) { 271 | if (scope === void 0) { scope = ''; } 272 | var list = []; 273 | utils_1.objectForEach(params, function (key) { 274 | if (params[key] instanceof Array) { 275 | if (exports.config.paramArrayType === ParamArrayType_1.default.OBJECT_PATH) { 276 | list.push.apply(list, __parametrize(params[key], key + ".")); 277 | } 278 | else if (exports.config.paramArrayType === ParamArrayType_1.default.COMMA_SEPARATED) { 279 | list.push({ key: "" + scope + key, value: params[key].join(',') }); 280 | } 281 | else if (exports.config.paramArrayType === ParamArrayType_1.default.MULTIPLE_PARAMS) { 282 | list.push.apply(list, params[key].map(function (param) { return ({ key: "" + scope + key, value: param }); })); 283 | } 284 | else if (exports.config.paramArrayType === ParamArrayType_1.default.PARAM_ARRAY) { 285 | list.push.apply(list, params[key].map(function (param) { return ({ key: "" + scope + key + "][", value: param }); })); 286 | } 287 | } 288 | else if (typeof params[key] === 'object') { 289 | list.push.apply(list, __parametrize(params[key], key + ".")); 290 | } 291 | else { 292 | list.push({ key: "" + scope + key, value: params[key] }); 293 | } 294 | }); 295 | return list; 296 | } 297 | function buildUrl(type, id, model, options) { 298 | var path = model 299 | ? (utils_1.getValue(model['endpoint']) || model['baseUrl'] || model.type) 300 | : type; 301 | var url = id ? path + "/" + id : "" + path; 302 | var params = __prepareFilters((options && options.filter) || {}).concat(__prepareSort(options && options.sort), __prepareIncludes(options && options.include), __prepareFields((options && options.fields) || {}), __prepareRawParams((options && options.params) || [])); 303 | return __appendParams(prefixUrl(url), params); 304 | } 305 | exports.buildUrl = buildUrl; 306 | -------------------------------------------------------------------------------- /dist/Record.d.ts: -------------------------------------------------------------------------------- 1 | import { IModel, Model } from 'mobx-collection-store'; 2 | import IDictionary from './interfaces/IDictionary'; 3 | import IRequestOptions from './interfaces/IRequestOptions'; 4 | import * as JsonApi from './interfaces/JsonApi'; 5 | import { Response } from './Response'; 6 | export declare class Record extends Model implements IModel { 7 | /** 8 | * Type property of the record class 9 | * 10 | * @static 11 | * 12 | * @memberOf Record 13 | */ 14 | static typeAttribute: string[]; 15 | /** 16 | * ID property of the record class 17 | * 18 | * @static 19 | * 20 | * @memberOf Record 21 | */ 22 | static idAttribute: string[]; 23 | /** 24 | * Should the autogenerated ID be sent to the server when creating a record 25 | * 26 | * @static 27 | * @type {boolean} 28 | * @memberOf Record 29 | */ 30 | static useAutogeneratedIds: boolean; 31 | /** 32 | * Endpoint for API requests if there is no self link 33 | * 34 | * @static 35 | * @type {string|() => string} 36 | * @memberOf Record 37 | */ 38 | static endpoint: string | (() => string); 39 | 'static': typeof Record; 40 | /** 41 | * Internal metadata 42 | * 43 | * @private 44 | * @type {IInternal} 45 | * @memberOf Record 46 | */ 47 | private __internal; 48 | /** 49 | * Cache link fetch requests 50 | * 51 | * @private 52 | * @type {IDictionary>} 53 | * @memberOf Record 54 | */ 55 | private __relationshipLinkCache; 56 | /** 57 | * Cache link fetch requests 58 | * 59 | * @private 60 | * @type {IDictionary>} 61 | * @memberOf Record 62 | */ 63 | private __linkCache; 64 | /** 65 | * Get record relationship links 66 | * 67 | * @returns {IDictionary} Record relationship links 68 | * 69 | * @memberOf Record 70 | */ 71 | getRelationshipLinks(): IDictionary; 72 | /** 73 | * Fetch a relationship link 74 | * 75 | * @param {string} relationship Name of the relationship 76 | * @param {string} name Name of the link 77 | * @param {IRequestOptions} [options] Server options 78 | * @param {boolean} [force=false] Ignore the existing cache 79 | * @returns {Promise} Response promise 80 | * 81 | * @memberOf Record 82 | */ 83 | fetchRelationshipLink(relationship: string, name: string, options?: IRequestOptions, force?: boolean): Promise; 84 | /** 85 | * Get record metadata 86 | * 87 | * @returns {object} Record metadata 88 | * 89 | * @memberOf Record 90 | */ 91 | getMeta(): object; 92 | /** 93 | * Get record links 94 | * 95 | * @returns {IDictionary} Record links 96 | * 97 | * @memberOf Record 98 | */ 99 | getLinks(): IDictionary; 100 | /** 101 | * Fetch a record link 102 | * 103 | * @param {string} name Name of the link 104 | * @param {IRequestOptions} [options] Server options 105 | * @param {boolean} [force=false] Ignore the existing cache 106 | * @returns {Promise} Response promise 107 | * 108 | * @memberOf Record 109 | */ 110 | fetchLink(name: string, options?: IRequestOptions, force?: boolean): Promise; 111 | /** 112 | * Get the persisted state 113 | * 114 | * @readonly 115 | * @private 116 | * @type {boolean} 117 | * @memberOf Record 118 | */ 119 | /** 120 | * Set the persisted state 121 | * 122 | * @private 123 | * 124 | * @memberOf Record 125 | */ 126 | private __persisted; 127 | /** 128 | * Serialize the record into JSON API format 129 | * 130 | * @returns {JsonApi.IRecord} JSON API formated record 131 | * 132 | * @memberOf Record 133 | */ 134 | toJsonApi(): JsonApi.IRecord; 135 | /** 136 | * Saves (creates or updates) the record to the server 137 | * 138 | * @param {IRequestOptions} [options] Server options 139 | * @param {boolean} [ignoreSelf=false] Should the self link be ignored if it exists 140 | * @returns {Promise} Returns the record is successful or rejects with an error 141 | * 142 | * @memberOf Record 143 | */ 144 | save(options?: IRequestOptions, ignoreSelf?: boolean): Promise; 145 | saveRelationship(relationship: string, options?: IRequestOptions): Promise; 146 | /** 147 | * Remove the records from the server and store 148 | * 149 | * @param {IRequestOptions} [options] Server options 150 | * @param {boolean} [ignoreSelf=false] Should the self link be ignored if it exists 151 | * @returns {Promise} Resolves true if successfull or rejects if there was an error 152 | * 153 | * @memberOf Record 154 | */ 155 | remove(options?: IRequestOptions, ignoreSelf?: boolean): Promise; 156 | /** 157 | * Set the persisted status of the record 158 | * 159 | * @param {boolean} state Is the record persisted on the server 160 | * 161 | * @memberOf Record 162 | */ 163 | setPersisted(state: boolean): void; 164 | /** 165 | * Get the persisted status of the record 166 | * 167 | * @memberOf Record 168 | */ 169 | getPersisted(): boolean; 170 | /** 171 | * Get the URL that should be used for the API calls 172 | * 173 | * @private 174 | * @returns {string} API URL 175 | * 176 | * @memberOf Record 177 | */ 178 | private __getUrl; 179 | } 180 | -------------------------------------------------------------------------------- /dist/Record.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | extendStatics(d, b); 11 | function __() { this.constructor = d; } 12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 13 | }; 14 | })(); 15 | Object.defineProperty(exports, "__esModule", { value: true }); 16 | var mobx_collection_store_1 = require("mobx-collection-store"); 17 | var NetworkUtils_1 = require("./NetworkUtils"); 18 | var utils_1 = require("./utils"); 19 | var Record = /** @class */ (function (_super) { 20 | __extends(Record, _super); 21 | function Record() { 22 | var _this = _super !== null && _super.apply(this, arguments) || this; 23 | /** 24 | * Cache link fetch requests 25 | * 26 | * @private 27 | * @type {IDictionary>} 28 | * @memberOf Record 29 | */ 30 | _this.__relationshipLinkCache = {}; 31 | /** 32 | * Cache link fetch requests 33 | * 34 | * @private 35 | * @type {IDictionary>} 36 | * @memberOf Record 37 | */ 38 | _this.__linkCache = {}; 39 | return _this; 40 | } 41 | /** 42 | * Get record relationship links 43 | * 44 | * @returns {IDictionary} Record relationship links 45 | * 46 | * @memberOf Record 47 | */ 48 | Record.prototype.getRelationshipLinks = function () { 49 | return this.__internal && this.__internal.relationships; 50 | }; 51 | /** 52 | * Fetch a relationship link 53 | * 54 | * @param {string} relationship Name of the relationship 55 | * @param {string} name Name of the link 56 | * @param {IRequestOptions} [options] Server options 57 | * @param {boolean} [force=false] Ignore the existing cache 58 | * @returns {Promise} Response promise 59 | * 60 | * @memberOf Record 61 | */ 62 | Record.prototype.fetchRelationshipLink = function (relationship, name, options, force) { 63 | if (force === void 0) { force = false; } 64 | this.__relationshipLinkCache[relationship] = this.__relationshipLinkCache[relationship] || {}; 65 | /* istanbul ignore else */ 66 | if (!(name in this.__relationshipLinkCache) || force) { 67 | var link = ('relationships' in this.__internal && 68 | relationship in this.__internal.relationships && 69 | name in this.__internal.relationships[relationship]) ? this.__internal.relationships[relationship][name] : null; 70 | var headers = options && options.headers; 71 | this.__relationshipLinkCache[relationship][name] = NetworkUtils_1.fetchLink(link, this.__collection, headers, options); 72 | } 73 | return this.__relationshipLinkCache[relationship][name]; 74 | }; 75 | /** 76 | * Get record metadata 77 | * 78 | * @returns {object} Record metadata 79 | * 80 | * @memberOf Record 81 | */ 82 | Record.prototype.getMeta = function () { 83 | return this.__internal && this.__internal.meta; 84 | }; 85 | /** 86 | * Get record links 87 | * 88 | * @returns {IDictionary} Record links 89 | * 90 | * @memberOf Record 91 | */ 92 | Record.prototype.getLinks = function () { 93 | return this.__internal && this.__internal.links; 94 | }; 95 | /** 96 | * Fetch a record link 97 | * 98 | * @param {string} name Name of the link 99 | * @param {IRequestOptions} [options] Server options 100 | * @param {boolean} [force=false] Ignore the existing cache 101 | * @returns {Promise} Response promise 102 | * 103 | * @memberOf Record 104 | */ 105 | Record.prototype.fetchLink = function (name, options, force) { 106 | var _this = this; 107 | if (force === void 0) { force = false; } 108 | if (!(name in this.__linkCache) || force) { 109 | var link = ('links' in this.__internal && name in this.__internal.links) ? 110 | this.__internal.links[name] : null; 111 | this.__linkCache[name] = NetworkUtils_1.fetchLink(link, this.__collection, options && options.headers, options); 112 | } 113 | var request = this.__linkCache[name]; 114 | if (this['__queue__']) { 115 | request = this.__linkCache[name].then(function (response) { 116 | var related = _this['__related__']; 117 | var prop = _this['__prop__']; 118 | var record = response.data; 119 | if (record && 120 | record.getRecordType() !== _this.getRecordType() && 121 | record.getRecordType() === related.getRecordType()) { 122 | /* istanbul ignore if */ 123 | if (prop) { 124 | related[prop] = record; 125 | return response; 126 | } 127 | related.__persisted = true; 128 | return response.replaceData(related); 129 | } 130 | return response; 131 | }); 132 | } 133 | return request; 134 | }; 135 | Object.defineProperty(Record.prototype, "__persisted", { 136 | /** 137 | * Get the persisted state 138 | * 139 | * @readonly 140 | * @private 141 | * @type {boolean} 142 | * @memberOf Record 143 | */ 144 | get: function () { 145 | return (this.__internal && this.__internal.persisted) || false; 146 | }, 147 | /** 148 | * Set the persisted state 149 | * 150 | * @private 151 | * 152 | * @memberOf Record 153 | */ 154 | set: function (state) { 155 | this.__internal.persisted = state; 156 | }, 157 | enumerable: true, 158 | configurable: true 159 | }); 160 | /** 161 | * Serialize the record into JSON API format 162 | * 163 | * @returns {JsonApi.IRecord} JSON API formated record 164 | * 165 | * @memberOf Record 166 | */ 167 | Record.prototype.toJsonApi = function () { 168 | var _this = this; 169 | var attributes = this.toJS(); 170 | var useAutogenerated = this.static['useAutogeneratedIds']; 171 | var data = { 172 | attributes: attributes, 173 | id: (this.__persisted || useAutogenerated) ? this.getRecordId() : undefined, 174 | type: this.getRecordType(), 175 | }; 176 | var refs = this['__refs']; 177 | utils_1.objectForEach(refs, function (key) { 178 | data.relationships = data.relationships || {}; 179 | var rel = utils_1.mapItems(_this[key + "Id"], function (id) { 180 | if (!id && id !== 0) { 181 | return null; 182 | } 183 | return { id: id, type: refs[key] }; 184 | }); 185 | data.relationships[key] = { data: rel }; 186 | delete data.attributes[key]; 187 | delete data.attributes[key + "Id"]; 188 | delete data.attributes[key + "Meta"]; 189 | }); 190 | delete data.attributes.__internal; 191 | delete data.attributes.__type__; 192 | return data; 193 | }; 194 | /** 195 | * Saves (creates or updates) the record to the server 196 | * 197 | * @param {IRequestOptions} [options] Server options 198 | * @param {boolean} [ignoreSelf=false] Should the self link be ignored if it exists 199 | * @returns {Promise} Returns the record is successful or rejects with an error 200 | * 201 | * @memberOf Record 202 | */ 203 | Record.prototype.save = function (options, ignoreSelf) { 204 | if (ignoreSelf === void 0) { ignoreSelf = false; } 205 | var store = this.__collection; 206 | var data = this.toJsonApi(); 207 | var requestMethod = this.__persisted ? NetworkUtils_1.update : NetworkUtils_1.create; 208 | return requestMethod(store, this.__getUrl(options, ignoreSelf), { data: data }, options && options.headers) 209 | .then(NetworkUtils_1.handleResponse(this)); 210 | }; 211 | Record.prototype.saveRelationship = function (relationship, options) { 212 | var link = ('relationships' in this.__internal && 213 | relationship in this.__internal.relationships && 214 | 'self' in this.__internal.relationships[relationship]) ? this.__internal.relationships[relationship]['self'] : null; 215 | /* istanbul ignore if */ 216 | if (!link) { 217 | throw new Error('The relationship doesn\'t have a defined link'); 218 | } 219 | var store = this.__collection; 220 | /* istanbul ignore next */ 221 | var href = typeof link === 'object' ? link.href : link; 222 | var type = this['__refs'][relationship]; 223 | var data = utils_1.mapItems(this[relationship + "Id"], function (id) { return ({ id: id, type: type }); }); 224 | return NetworkUtils_1.update(store, href, { data: data }, options && options.headers) 225 | .then(NetworkUtils_1.handleResponse(this, relationship)); 226 | }; 227 | /** 228 | * Remove the records from the server and store 229 | * 230 | * @param {IRequestOptions} [options] Server options 231 | * @param {boolean} [ignoreSelf=false] Should the self link be ignored if it exists 232 | * @returns {Promise} Resolves true if successfull or rejects if there was an error 233 | * 234 | * @memberOf Record 235 | */ 236 | Record.prototype.remove = function (options, ignoreSelf) { 237 | var _this = this; 238 | if (ignoreSelf === void 0) { ignoreSelf = false; } 239 | var store = this.__collection; 240 | if (!this.__persisted) { 241 | this.__collection.remove(this.getRecordType(), this.getRecordId()); 242 | return Promise.resolve(true); 243 | } 244 | return NetworkUtils_1.remove(store, this.__getUrl(options, ignoreSelf), options && options.headers) 245 | .then(function (response) { 246 | /* istanbul ignore if */ 247 | if (response.error) { 248 | throw response.error; 249 | } 250 | _this.__persisted = false; 251 | if (_this.__collection) { 252 | _this.__collection.remove(_this.getRecordType(), _this.getRecordId()); 253 | } 254 | return true; 255 | }); 256 | }; 257 | /** 258 | * Set the persisted status of the record 259 | * 260 | * @param {boolean} state Is the record persisted on the server 261 | * 262 | * @memberOf Record 263 | */ 264 | Record.prototype.setPersisted = function (state) { 265 | this.__persisted = state; 266 | }; 267 | /** 268 | * Get the persisted status of the record 269 | * 270 | * @memberOf Record 271 | */ 272 | Record.prototype.getPersisted = function () { 273 | return this.__persisted; 274 | }; 275 | /** 276 | * Get the URL that should be used for the API calls 277 | * 278 | * @private 279 | * @returns {string} API URL 280 | * 281 | * @memberOf Record 282 | */ 283 | Record.prototype.__getUrl = function (options, ignoreSelf) { 284 | var links = this.getLinks(); 285 | if (!ignoreSelf && links && links.self) { 286 | var self_1 = links.self; 287 | /* istanbul ignore next */ 288 | return typeof self_1 === 'string' ? self_1 : self_1.href; 289 | } 290 | /* istanbul ignore next */ 291 | var type = utils_1.getValue(this.static.endpoint) || this.getRecordType() || this.static.type; 292 | return NetworkUtils_1.buildUrl(type, this.__persisted ? this.getRecordId() : null, null, options); 293 | }; 294 | /** 295 | * Type property of the record class 296 | * 297 | * @static 298 | * 299 | * @memberOf Record 300 | */ 301 | Record.typeAttribute = ['__internal', 'type']; 302 | /** 303 | * ID property of the record class 304 | * 305 | * @static 306 | * 307 | * @memberOf Record 308 | */ 309 | Record.idAttribute = ['__internal', 'id']; 310 | /** 311 | * Should the autogenerated ID be sent to the server when creating a record 312 | * 313 | * @static 314 | * @type {boolean} 315 | * @memberOf Record 316 | */ 317 | Record.useAutogeneratedIds = false; 318 | return Record; 319 | }(mobx_collection_store_1.Model)); 320 | exports.Record = Record; 321 | -------------------------------------------------------------------------------- /dist/Response.d.ts: -------------------------------------------------------------------------------- 1 | import { IModel } from 'mobx-collection-store'; 2 | import IDictionary from './interfaces/IDictionary'; 3 | import IHeaders from './interfaces/IHeaders'; 4 | import IRawResponse from './interfaces/IRawResponse'; 5 | import IRequestOptions from './interfaces/IRequestOptions'; 6 | import IResponseHeaders from './interfaces/IResponseHeaders'; 7 | import * as JsonApi from './interfaces/JsonApi'; 8 | import { Record } from './Record'; 9 | import { Store } from './Store'; 10 | export declare class Response { 11 | /** 12 | * API response data (synced with the store) 13 | * 14 | * @type {(IModel|Array)} 15 | * @memberOf Response 16 | */ 17 | data?: IModel | Array; 18 | /** 19 | * API response metadata 20 | * 21 | * @type {object} 22 | * @memberOf Response 23 | */ 24 | meta?: object; 25 | /** 26 | * API reslonse links 27 | * 28 | * @type {object} 29 | * @memberOf Response 30 | */ 31 | links?: IDictionary; 32 | /** 33 | * The JSON API object returned by the server 34 | * 35 | * @type {JsonApi.IJsonApiObject} 36 | * @memberOf Response 37 | */ 38 | jsonapi?: JsonApi.IJsonApiObject; 39 | /** 40 | * Headers received from the API call 41 | * 42 | * @type {IResponseHeaders} 43 | * @memberOf Response 44 | */ 45 | headers?: IResponseHeaders; 46 | /** 47 | * Headers sent to the server 48 | * 49 | * @type {IHeaders} 50 | * @memberOf Response 51 | */ 52 | requestHeaders?: IHeaders; 53 | /** 54 | * Request error 55 | * 56 | * @type {(Array|Error)} 57 | * @memberOf Response 58 | */ 59 | error?: Array | Error; 60 | /** 61 | * First data page 62 | * 63 | * @type {Promise} 64 | * @memberOf Response 65 | */ 66 | first: Promise; 67 | /** 68 | * Previous data page 69 | * 70 | * @type {Promise} 71 | * @memberOf Response 72 | */ 73 | prev: Promise; 74 | /** 75 | * Next data page 76 | * 77 | * @type {Promise} 78 | * @memberOf Response 79 | */ 80 | next: Promise; 81 | /** 82 | * Last data page 83 | * 84 | * @type {Promise} 85 | * @memberOf Response 86 | */ 87 | last: Promise; 88 | /** 89 | * Received HTTP status 90 | * 91 | * @type {number} 92 | * @memberOf Response 93 | */ 94 | status: number; 95 | /** 96 | * Related Store 97 | * 98 | * @private 99 | * @type {Store} 100 | * @memberOf Response 101 | */ 102 | private __store; 103 | /** 104 | * Server options 105 | * 106 | * @private 107 | * @type {IRequestOptions} 108 | * @memberOf Response 109 | */ 110 | private __options; 111 | /** 112 | * Original server response 113 | * 114 | * @private 115 | * @type {IRawResponse} 116 | * @memberOf Response 117 | */ 118 | private __response; 119 | /** 120 | * Cache used for the link requests 121 | * 122 | * @private 123 | * @type {IDictionary>} 124 | * @memberOf Response 125 | */ 126 | private __cache; 127 | constructor(response: IRawResponse, store?: Store, options?: IRequestOptions, overrideData?: IModel | Array); 128 | /** 129 | * Replace the response record with a different record. Used to replace a record while keeping the same reference 130 | * 131 | * @param {IModel} data New data 132 | * @returns {Response} 133 | * 134 | * @memberOf Response 135 | */ 136 | replaceData(data: Record): Response; 137 | /** 138 | * Update references in the store 139 | * 140 | * @private 141 | * @param {any} type Record type 142 | * @param {any} oldId Old redord ID 143 | * @param {any} newId New record ID 144 | * @memberof Response 145 | */ 146 | private __updateStoreReferences; 147 | /** 148 | * Update models that reference the updated model 149 | * 150 | * @private 151 | * @param {any} oldId Old record ID 152 | * @param {any} newId new record ID 153 | * @memberof Response 154 | */ 155 | private __updateReferences; 156 | /** 157 | * Function called when a link is beeing fetched. The returned value is cached 158 | * 159 | * @private 160 | * @param {any} name Link name 161 | * @returns Promise that resolves with a Response object 162 | * 163 | * @memberOf Response 164 | */ 165 | private __fetchLink; 166 | } 167 | -------------------------------------------------------------------------------- /dist/Response.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | Object.defineProperty(exports, "__esModule", { value: true }); 9 | var mobx_1 = require("mobx"); 10 | var Record_1 = require("./Record"); 11 | var utils_1 = require("./utils"); 12 | var NetworkUtils_1 = require("./NetworkUtils"); 13 | var Response = /** @class */ (function () { 14 | function Response(response, store, options, overrideData) { 15 | var _this = this; 16 | /** 17 | * Cache used for the link requests 18 | * 19 | * @private 20 | * @type {IDictionary>} 21 | * @memberOf Response 22 | */ 23 | this.__cache = {}; 24 | this.__store = store; 25 | this.__options = options; 26 | this.__response = response; 27 | this.status = response.status; 28 | if (store) { 29 | this.data = overrideData ? store.add(overrideData) : store.sync(response.data); 30 | } 31 | else if (response.data) { 32 | // The case when a record is not in a store and save/remove are used 33 | var resp = response.data; 34 | /* istanbul ignore if */ 35 | if (resp.data instanceof Array) { 36 | throw new Error('A save/remove operation should not return an array of results'); 37 | } 38 | this.data = overrideData || new Record_1.Record(utils_1.flattenRecord(resp.data)); 39 | } 40 | this.meta = (response.data && response.data.meta) || {}; 41 | this.links = (response.data && response.data.links) || {}; 42 | this.jsonapi = (response.data && response.data.jsonapi) || {}; 43 | this.headers = response.headers; 44 | this.requestHeaders = response.requestHeaders; 45 | this.error = (response.data && response.data.errors) || response.error; 46 | var linkGetter = {}; 47 | Object.keys(this.links).forEach(function (link) { 48 | linkGetter[link] = mobx_1.computed(function () { return _this.__fetchLink(link); }); 49 | }); 50 | mobx_1.extendObservable(this, linkGetter); 51 | Object.freeze(this); 52 | if (this.error) { 53 | throw this; 54 | } 55 | } 56 | /** 57 | * Replace the response record with a different record. Used to replace a record while keeping the same reference 58 | * 59 | * @param {IModel} data New data 60 | * @returns {Response} 61 | * 62 | * @memberOf Response 63 | */ 64 | Response.prototype.replaceData = function (data) { 65 | var record = this.data; 66 | if (record === data) { 67 | return this; 68 | } 69 | var oldId = data.getRecordId(); 70 | var newId = record.getRecordId(); 71 | var type = record.getRecordType(); 72 | if (this.__store) { 73 | this.__store.remove(type, newId); 74 | } 75 | data.update(record.toJS()); 76 | // TODO: Refactor this to avoid using mobx-collection-store internals 77 | data['__internal'].id = newId; 78 | this.__updateStoreReferences(type, oldId, newId); 79 | return new Response(this.__response, this.__store, this.__options, data); 80 | }; 81 | /** 82 | * Update references in the store 83 | * 84 | * @private 85 | * @param {any} type Record type 86 | * @param {any} oldId Old redord ID 87 | * @param {any} newId New record ID 88 | * @memberof Response 89 | */ 90 | Response.prototype.__updateStoreReferences = function (type, oldId, newId) { 91 | if (this.__store) { 92 | var modelHash = this.__store['__modelHash'][type]; 93 | var oldModel = modelHash[oldId]; 94 | modelHash[newId] = oldModel; 95 | delete modelHash[oldId]; 96 | this.__updateReferences(type, oldId, newId); 97 | } 98 | }; 99 | /** 100 | * Update models that reference the updated model 101 | * 102 | * @private 103 | * @param {any} oldId Old record ID 104 | * @param {any} newId new record ID 105 | * @memberof Response 106 | */ 107 | Response.prototype.__updateReferences = function (type, oldId, newId) { 108 | this.__store['__data'].map(function (model) { 109 | var keyList = utils_1.keys(model['__data']); 110 | keyList.map(function (key) { 111 | var keyId = key + "Id"; 112 | var refs = model.__refs || model.static.refs; 113 | var refsType = refs && refs[key]; 114 | if (key in model && keyId in model && refsType === type) { 115 | if (mobx_1.isObservableArray(model[keyId])) { 116 | var index = model[keyId].indexOf(oldId); 117 | if (index > -1) { 118 | model[keyId][index] = newId; 119 | } 120 | } 121 | else if (model[keyId] === oldId) { 122 | model[keyId] = newId; 123 | } 124 | } 125 | }); 126 | }); 127 | }; 128 | /** 129 | * Function called when a link is beeing fetched. The returned value is cached 130 | * 131 | * @private 132 | * @param {any} name Link name 133 | * @returns Promise that resolves with a Response object 134 | * 135 | * @memberOf Response 136 | */ 137 | Response.prototype.__fetchLink = function (name) { 138 | if (!this.__cache[name]) { 139 | /* istanbul ignore next */ 140 | var link = name in this.links ? this.links[name] : null; 141 | this.__cache[name] = NetworkUtils_1.fetchLink(link, this.__store, this.requestHeaders, this.__options); 142 | } 143 | return this.__cache[name]; 144 | }; 145 | __decorate([ 146 | mobx_1.action 147 | ], Response.prototype, "replaceData", null); 148 | return Response; 149 | }()); 150 | exports.Response = Response; 151 | -------------------------------------------------------------------------------- /dist/Store.d.ts: -------------------------------------------------------------------------------- 1 | import { IModel } from 'mobx-collection-store'; 2 | import IRequestOptions from './interfaces/IRequestOptions'; 3 | import * as JsonApi from './interfaces/JsonApi'; 4 | import { NetworkStore } from './NetworkStore'; 5 | import { Record } from './Record'; 6 | import { Response } from './Response'; 7 | export declare class Store extends NetworkStore { 8 | /** 9 | * List of Models that will be used in the collection 10 | * 11 | * @static 12 | * 13 | * @memberOf Store 14 | */ 15 | static types: (typeof Record)[]; 16 | /** 17 | * Should the cache be used for API calls when possible 18 | * 19 | * @static 20 | * 21 | * @memberof Store 22 | */ 23 | static cache: boolean; 24 | static: typeof Store; 25 | /** 26 | * Cache async actions (can be overriden with force=true) 27 | * 28 | * @private 29 | * 30 | * @memberOf Store 31 | */ 32 | private __cache; 33 | /** 34 | * Import the JSON API data into the store 35 | * 36 | * @param {IJsonApiResponse} body - JSON API response 37 | * @returns {(IModel|Array)} - Models parsed from body.data 38 | * 39 | * @memberOf Store 40 | */ 41 | sync(body: JsonApi.IResponse): IModel | Array; 42 | /** 43 | * Fetch the records with the given type and id 44 | * 45 | * @param {string} type Record type 46 | * @param {number|string} type Record id 47 | * @param {boolean} [force] Force fetch (currently not used) 48 | * @param {IRequestOptions} [options] Server options 49 | * @returns {Promise} Resolves with the Response object or rejects with an error 50 | * 51 | * @memberOf Store 52 | */ 53 | fetch(type: string, id: number | string, force?: boolean, options?: IRequestOptions): Promise; 54 | /** 55 | * Fetch the first page of records of the given type 56 | * 57 | * @param {string} type Record type 58 | * @param {boolean} [force] Force fetch (currently not used) 59 | * @param {IRequestOptions} [options] Server options 60 | * @returns {Promise} Resolves with the Response object or rejects with an error 61 | * 62 | * @memberOf Store 63 | */ 64 | fetchAll(type: string, force?: boolean, options?: IRequestOptions): Promise; 65 | /** 66 | * Destroy a record (API & store) 67 | * 68 | * @param {string} type Record type 69 | * @param {(number|string)} id Record id 70 | * @param {IRequestOptions} [options] Server options 71 | * @returns {Promise} Resolves true or rejects with an error 72 | * 73 | * @memberOf Store 74 | */ 75 | destroy(type: string, id: number | string, options?: IRequestOptions): Promise; 76 | reset(): void; 77 | request(url: string, method?: string, data?: object, options?: IRequestOptions): Promise; 78 | removeAll(type: string): Array; 79 | /** 80 | * Make the request and handle the errors 81 | * 82 | * @param {IQueryParams} query Request query info 83 | * @param {IRequestOptions} [options] Server options 84 | * @returns {Promise} Resolves with the Response object or rejects with an error 85 | * 86 | * @memberof Store 87 | */ 88 | private __doFetch; 89 | /** 90 | * Function used to handle response errors 91 | * 92 | * @private 93 | * @param {Response} response API response 94 | * @returns API response 95 | * 96 | * @memberOf Store 97 | */ 98 | private __handleErrors; 99 | /** 100 | * Add a new JSON API record to the store 101 | * 102 | * @private 103 | * @param {IJsonApiRecord} obj - Object to be added 104 | * @returns {IModel} 105 | * 106 | * @memberOf Store 107 | */ 108 | private __addRecord; 109 | /** 110 | * Update the relationships between models 111 | * 112 | * @private 113 | * @param {IJsonApiRecord} obj - Object to be updated 114 | * @returns {void} 115 | * 116 | * @memberOf Store 117 | */ 118 | private __updateRelationships; 119 | /** 120 | * Iterate trough JSON API response models 121 | * 122 | * @private 123 | * @param {IJsonApiResponse} body - JSON API response 124 | * @param {Function} fn - Function to call for every instance 125 | * @returns 126 | * 127 | * @memberOf Store 128 | */ 129 | private __iterateEntries; 130 | } 131 | -------------------------------------------------------------------------------- /dist/Store.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | extendStatics(d, b); 11 | function __() { this.constructor = d; } 12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 13 | }; 14 | })(); 15 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 16 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 17 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 18 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 19 | return c > 3 && r && Object.defineProperty(target, key, r), r; 20 | }; 21 | Object.defineProperty(exports, "__esModule", { value: true }); 22 | var mobx_1 = require("mobx"); 23 | var NetworkStore_1 = require("./NetworkStore"); 24 | var NetworkUtils_1 = require("./NetworkUtils"); 25 | var Record_1 = require("./Record"); 26 | var utils_1 = require("./utils"); 27 | var Store = /** @class */ (function (_super) { 28 | __extends(Store, _super); 29 | function Store() { 30 | var _this = _super !== null && _super.apply(this, arguments) || this; 31 | /** 32 | * Cache async actions (can be overriden with force=true) 33 | * 34 | * @private 35 | * 36 | * @memberOf Store 37 | */ 38 | _this.__cache = { 39 | fetch: {}, 40 | fetchAll: {}, 41 | }; 42 | return _this; 43 | } 44 | /** 45 | * Import the JSON API data into the store 46 | * 47 | * @param {IJsonApiResponse} body - JSON API response 48 | * @returns {(IModel|Array)} - Models parsed from body.data 49 | * 50 | * @memberOf Store 51 | */ 52 | Store.prototype.sync = function (body) { 53 | var data = this.__iterateEntries(body, this.__addRecord.bind(this)); 54 | this.__iterateEntries(body, this.__updateRelationships.bind(this)); 55 | return data; 56 | }; 57 | /** 58 | * Fetch the records with the given type and id 59 | * 60 | * @param {string} type Record type 61 | * @param {number|string} type Record id 62 | * @param {boolean} [force] Force fetch (currently not used) 63 | * @param {IRequestOptions} [options] Server options 64 | * @returns {Promise} Resolves with the Response object or rejects with an error 65 | * 66 | * @memberOf Store 67 | */ 68 | Store.prototype.fetch = function (type, id, force, options) { 69 | var _this = this; 70 | var query = this.__prepareQuery(type, id, null, options); 71 | if (!this.static.cache) { 72 | return this.__doFetch(query, options); 73 | } 74 | this.__cache.fetch[type] = this.__cache.fetch[type] || {}; 75 | // TODO: Should we fake the cache if the record already exists? 76 | if (force || !(query.url in this.__cache.fetch[type])) { 77 | this.__cache.fetch[type][query.url] = this.__doFetch(query, options) 78 | .catch(function (e) { 79 | // Don't cache if there was an error 80 | delete _this.__cache.fetch[type][query.url]; 81 | throw e; 82 | }); 83 | } 84 | return this.__cache.fetch[type][query.url]; 85 | }; 86 | /** 87 | * Fetch the first page of records of the given type 88 | * 89 | * @param {string} type Record type 90 | * @param {boolean} [force] Force fetch (currently not used) 91 | * @param {IRequestOptions} [options] Server options 92 | * @returns {Promise} Resolves with the Response object or rejects with an error 93 | * 94 | * @memberOf Store 95 | */ 96 | Store.prototype.fetchAll = function (type, force, options) { 97 | var _this = this; 98 | var query = this.__prepareQuery(type, null, null, options); 99 | if (!this.static.cache) { 100 | return this.__doFetch(query, options); 101 | } 102 | this.__cache.fetchAll[type] = this.__cache.fetchAll[type] || {}; 103 | if (force || !(query.url in this.__cache.fetchAll[type])) { 104 | this.__cache.fetchAll[type][query.url] = this.__doFetch(query, options) 105 | .catch(function (e) { 106 | // Don't cache if there was an error 107 | delete _this.__cache.fetchAll[type][query.url]; 108 | throw e; 109 | }); 110 | } 111 | return this.__cache.fetchAll[type][query.url]; 112 | }; 113 | /** 114 | * Destroy a record (API & store) 115 | * 116 | * @param {string} type Record type 117 | * @param {(number|string)} id Record id 118 | * @param {IRequestOptions} [options] Server options 119 | * @returns {Promise} Resolves true or rejects with an error 120 | * 121 | * @memberOf Store 122 | */ 123 | Store.prototype.destroy = function (type, id, options) { 124 | var model = this.find(type, id); 125 | if (model) { 126 | return model.remove(options); 127 | } 128 | return Promise.resolve(true); 129 | }; 130 | Store.prototype.reset = function () { 131 | _super.prototype.reset.call(this); 132 | this.__cache.fetch = {}; 133 | this.__cache.fetchAll = {}; 134 | }; 135 | Store.prototype.request = function (url, method, data, options) { 136 | if (method === void 0) { method = 'GET'; } 137 | return NetworkUtils_1.fetch({ url: NetworkUtils_1.prefixUrl(url), options: options, data: data, method: method, store: this }); 138 | }; 139 | Store.prototype.removeAll = function (type) { 140 | var models = _super.prototype.removeAll.call(this, type); 141 | this.__cache.fetch[type] = {}; 142 | this.__cache.fetchAll[type] = {}; 143 | return models; 144 | }; 145 | /** 146 | * Make the request and handle the errors 147 | * 148 | * @param {IQueryParams} query Request query info 149 | * @param {IRequestOptions} [options] Server options 150 | * @returns {Promise} Resolves with the Response object or rejects with an error 151 | * 152 | * @memberof Store 153 | */ 154 | Store.prototype.__doFetch = function (query, options) { 155 | return NetworkUtils_1.read(this, query.url, query.headers, options).then(this.__handleErrors); 156 | }; 157 | /** 158 | * Function used to handle response errors 159 | * 160 | * @private 161 | * @param {Response} response API response 162 | * @returns API response 163 | * 164 | * @memberOf Store 165 | */ 166 | Store.prototype.__handleErrors = function (response) { 167 | /* istanbul ignore if */ 168 | if (response.error) { 169 | throw response.error; 170 | } 171 | return response; 172 | }; 173 | /** 174 | * Add a new JSON API record to the store 175 | * 176 | * @private 177 | * @param {IJsonApiRecord} obj - Object to be added 178 | * @returns {IModel} 179 | * 180 | * @memberOf Store 181 | */ 182 | Store.prototype.__addRecord = function (obj) { 183 | var type = obj.type, id = obj.id; 184 | var record = this.find(type, id); 185 | var flattened = utils_1.flattenRecord(obj); 186 | if (record) { 187 | record.update(flattened); 188 | } 189 | else if (this.static.types.filter(function (item) { return item.type === obj.type; }).length) { 190 | record = this.add(flattened, obj.type); 191 | } 192 | else { 193 | record = new Record_1.Record(flattened); 194 | this.add(record); 195 | } 196 | // In case a record is not a real record 197 | // TODO: Figure out when this happens and try to handle it better 198 | /* istanbul ignore else */ 199 | if (record && typeof record.setPersisted === 'function') { 200 | record.setPersisted(true); 201 | } 202 | return record; 203 | }; 204 | /** 205 | * Update the relationships between models 206 | * 207 | * @private 208 | * @param {IJsonApiRecord} obj - Object to be updated 209 | * @returns {void} 210 | * 211 | * @memberOf Store 212 | */ 213 | Store.prototype.__updateRelationships = function (obj) { 214 | var _this = this; 215 | var record = this.find(obj.type, obj.id); 216 | var refs = obj.relationships ? Object.keys(obj.relationships) : []; 217 | refs.forEach(function (ref) { 218 | if (!('data' in obj.relationships[ref])) { 219 | return; 220 | } 221 | var items = obj.relationships[ref].data; 222 | if (items instanceof Array && items.length < 1) { 223 | if (!(ref in record) || ref in record['__data']) { // Hack to check if it's not a back ref 224 | record.assignRef(ref, []); 225 | } 226 | } 227 | else if (record) { 228 | if (items) { 229 | var models = utils_1.mapItems(items, function (_a) { 230 | var id = _a.id, type = _a.type; 231 | return _this.find(type, id) || id; 232 | }); 233 | var itemType = items instanceof Array ? items[0].type : items.type; 234 | record.assignRef(ref, models, itemType); 235 | } 236 | else { 237 | record.assignRef(ref, null); 238 | } 239 | } 240 | }); 241 | }; 242 | /** 243 | * Iterate trough JSON API response models 244 | * 245 | * @private 246 | * @param {IJsonApiResponse} body - JSON API response 247 | * @param {Function} fn - Function to call for every instance 248 | * @returns 249 | * 250 | * @memberOf Store 251 | */ 252 | Store.prototype.__iterateEntries = function (body, fn) { 253 | utils_1.mapItems((body && body.included) || [], fn); 254 | return utils_1.mapItems((body && body.data) || [], fn); 255 | }; 256 | /** 257 | * List of Models that will be used in the collection 258 | * 259 | * @static 260 | * 261 | * @memberOf Store 262 | */ 263 | Store.types = [Record_1.Record]; 264 | /** 265 | * Should the cache be used for API calls when possible 266 | * 267 | * @static 268 | * 269 | * @memberof Store 270 | */ 271 | Store.cache = true; 272 | __decorate([ 273 | mobx_1.action 274 | ], Store.prototype, "sync", null); 275 | return Store; 276 | }(NetworkStore_1.NetworkStore)); 277 | exports.Store = Store; 278 | -------------------------------------------------------------------------------- /dist/enums/ParamArrayType.d.ts: -------------------------------------------------------------------------------- 1 | declare enum ParamArrayType { 2 | MULTIPLE_PARAMS = 0, 3 | COMMA_SEPARATED = 1, 4 | PARAM_ARRAY = 2, 5 | OBJECT_PATH = 3 6 | } 7 | export default ParamArrayType; 8 | -------------------------------------------------------------------------------- /dist/enums/ParamArrayType.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var ParamArrayType; 4 | (function (ParamArrayType) { 5 | ParamArrayType[ParamArrayType["MULTIPLE_PARAMS"] = 0] = "MULTIPLE_PARAMS"; 6 | ParamArrayType[ParamArrayType["COMMA_SEPARATED"] = 1] = "COMMA_SEPARATED"; 7 | ParamArrayType[ParamArrayType["PARAM_ARRAY"] = 2] = "PARAM_ARRAY"; 8 | ParamArrayType[ParamArrayType["OBJECT_PATH"] = 3] = "OBJECT_PATH"; 9 | })(ParamArrayType || (ParamArrayType = {})); 10 | exports.default = ParamArrayType; 11 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as JsonApi from './interfaces/JsonApi'; 2 | export { JsonApi }; 3 | export { Store } from './Store'; 4 | export { Record } from './Record'; 5 | export { Response } from './Response'; 6 | export * from './NetworkUtils'; 7 | export { default as ICache } from './interfaces/ICache'; 8 | export { default as IDictionary } from './interfaces/IDictionary'; 9 | export { default as IFilters } from './interfaces/IFilters'; 10 | export { default as IHeaders } from './interfaces/IHeaders'; 11 | export { default as IRawResponse } from './interfaces/IRawResponse'; 12 | export { default as IRequestOptions } from './interfaces/IRequestOptions'; 13 | export { default as IResponseHeaders } from './interfaces/IResponseHeaders'; 14 | export { default as ParamArrayType } from './enums/ParamArrayType'; 15 | export { ICollection, IModel, IModelConstructor, IReferences } from 'mobx-collection-store'; 16 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function __export(m) { 3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; 4 | } 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var JsonApi = require("./interfaces/JsonApi"); 7 | exports.JsonApi = JsonApi; 8 | var Store_1 = require("./Store"); 9 | exports.Store = Store_1.Store; 10 | var Record_1 = require("./Record"); 11 | exports.Record = Record_1.Record; 12 | var Response_1 = require("./Response"); 13 | exports.Response = Response_1.Response; 14 | __export(require("./NetworkUtils")); 15 | var ParamArrayType_1 = require("./enums/ParamArrayType"); 16 | exports.ParamArrayType = ParamArrayType_1.default; 17 | -------------------------------------------------------------------------------- /dist/interfaces/ICache.d.ts: -------------------------------------------------------------------------------- 1 | import IDictionary from './IDictionary'; 2 | import { Response } from '../Response'; 3 | interface ICache { 4 | fetchAll: IDictionary>>; 5 | fetch: IDictionary>>; 6 | } 7 | export default ICache; 8 | -------------------------------------------------------------------------------- /dist/interfaces/ICache.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/interfaces/IDictionary.d.ts: -------------------------------------------------------------------------------- 1 | interface IDictionary { 2 | [key: string]: T; 3 | } 4 | export default IDictionary; 5 | -------------------------------------------------------------------------------- /dist/interfaces/IDictionary.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/interfaces/IFilters.d.ts: -------------------------------------------------------------------------------- 1 | interface IFilters { 2 | [key: string]: number | string | Array | Array | IFilters; 3 | } 4 | export default IFilters; 5 | -------------------------------------------------------------------------------- /dist/interfaces/IFilters.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/interfaces/IHeaders.d.ts: -------------------------------------------------------------------------------- 1 | import IDictionary from './IDictionary'; 2 | declare type IHeaders = IDictionary; 3 | export default IHeaders; 4 | -------------------------------------------------------------------------------- /dist/interfaces/IHeaders.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/interfaces/IJsonApiIdentifier.d.ts: -------------------------------------------------------------------------------- 1 | interface IJsonApiIdentifier { 2 | id: number | string; 3 | type: string; 4 | } 5 | export default IJsonApiIdentifier; 6 | -------------------------------------------------------------------------------- /dist/interfaces/IJsonApiIdentifier.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | -------------------------------------------------------------------------------- /dist/interfaces/IJsonApiRecord.d.ts: -------------------------------------------------------------------------------- 1 | import IDictionary from './IDictionary'; 2 | import IJsonApiRelationship from './IJsonApiRelationship'; 3 | interface IJsonApiRecord { 4 | id: number | string; 5 | type: string; 6 | attributes: IDictionary; 7 | relationships?: IDictionary; 8 | } 9 | export default IJsonApiRecord; 10 | -------------------------------------------------------------------------------- /dist/interfaces/IJsonApiRecord.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | -------------------------------------------------------------------------------- /dist/interfaces/IJsonApiRelationship.d.ts: -------------------------------------------------------------------------------- 1 | import IDictionary from './IDictionary'; 2 | import IJsonApiIdentifier from './IJsonApiIdentifier'; 3 | interface IJsonApiRelationship { 4 | data?: IJsonApiIdentifier | Array; 5 | links?: IDictionary; 6 | } 7 | export default IJsonApiRelationship; 8 | -------------------------------------------------------------------------------- /dist/interfaces/IJsonApiRelationship.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | -------------------------------------------------------------------------------- /dist/interfaces/IJsonApiResponse.d.ts: -------------------------------------------------------------------------------- 1 | import IJsonApiRecord from './IJsonApiRecord'; 2 | interface IJsonApiResponse { 3 | data: IJsonApiRecord | Array; 4 | included?: Array; 5 | } 6 | export default IJsonApiResponse; 7 | -------------------------------------------------------------------------------- /dist/interfaces/IJsonApiResponse.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | -------------------------------------------------------------------------------- /dist/interfaces/IRawResponse.d.ts: -------------------------------------------------------------------------------- 1 | import { Store } from '../Store'; 2 | import IHeaders from './IHeaders'; 3 | import IResponseHeaders from './IResponseHeaders'; 4 | import * as JsonApi from './JsonApi'; 5 | interface IRawResponse { 6 | data?: JsonApi.IResponse; 7 | error?: Error; 8 | headers?: IResponseHeaders; 9 | requestHeaders?: IHeaders; 10 | status?: number; 11 | jsonapi?: JsonApi.IJsonApiObject; 12 | store?: Store; 13 | } 14 | export default IRawResponse; 15 | -------------------------------------------------------------------------------- /dist/interfaces/IRawResponse.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/interfaces/IRequestOptions.d.ts: -------------------------------------------------------------------------------- 1 | import IDictionary from './IDictionary'; 2 | import IFilters from './IFilters'; 3 | import IHeaders from './IHeaders'; 4 | interface IRequestOptions { 5 | headers?: IHeaders; 6 | include?: string | Array; 7 | filter?: IFilters; 8 | sort?: string | Array; 9 | fields?: IDictionary>; 10 | params?: Array<{ 11 | key: string; 12 | value: string; 13 | } | string>; 14 | } 15 | export default IRequestOptions; 16 | -------------------------------------------------------------------------------- /dist/interfaces/IRequestOptions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/interfaces/IResponseHeaders.d.ts: -------------------------------------------------------------------------------- 1 | interface IResponseHeaders { 2 | get(name: string): string; 3 | } 4 | export default IResponseHeaders; 5 | -------------------------------------------------------------------------------- /dist/interfaces/IResponseHeaders.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/interfaces/JsonApi.d.ts: -------------------------------------------------------------------------------- 1 | import IDictionary from './IDictionary'; 2 | interface IIdentifier { 3 | id?: number | string; 4 | type: string; 5 | } 6 | interface IJsonApiObject { 7 | version?: string; 8 | meta?: IDictionary; 9 | } 10 | declare type ILink = string | { 11 | href: string; 12 | meta: IDictionary; 13 | }; 14 | interface IError { 15 | id?: string | number; 16 | links?: { 17 | about: ILink; 18 | }; 19 | status?: number; 20 | code?: string; 21 | title?: string; 22 | detail?: string; 23 | source?: { 24 | pointer?: string; 25 | parameter?: string; 26 | }; 27 | meta?: IDictionary; 28 | } 29 | interface IRelationship { 30 | data?: IIdentifier | Array; 31 | links?: IDictionary; 32 | meta?: IDictionary; 33 | } 34 | interface IRecord extends IIdentifier { 35 | attributes: IDictionary; 36 | relationships?: IDictionary; 37 | links?: IDictionary; 38 | meta?: IDictionary; 39 | } 40 | interface IResponse { 41 | data?: IRecord | Array; 42 | errors?: Array; 43 | included?: Array; 44 | meta?: IDictionary; 45 | links?: IDictionary; 46 | jsonapi?: IJsonApiObject; 47 | } 48 | declare type IRequest = IResponse; 49 | export { IIdentifier, IJsonApiObject, ILink, IError, IRelationship, IRecord, IResponse, IRequest, }; 50 | -------------------------------------------------------------------------------- /dist/interfaces/JsonApi.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/utils.d.ts: -------------------------------------------------------------------------------- 1 | import IDictionary from './interfaces/IDictionary'; 2 | import * as JsonApi from './interfaces/JsonApi'; 3 | /** 4 | * Iterate trough object keys 5 | * 6 | * @param {object} obj - Object that needs to be iterated 7 | * @param {Function} fn - Function that should be called for every iteration 8 | */ 9 | export declare function objectForEach(obj: object, fn: Function): void; 10 | /** 11 | * Iterate trough one item or array of items and call the defined function 12 | * 13 | * @export 14 | * @template T 15 | * @param {(object|Array)} data - Data which needs to be iterated 16 | * @param {Function} fn - Function that needs to be callse 17 | * @returns {(T|Array)} - The result of iteration 18 | */ 19 | export declare function mapItems(data: object | Array, fn: Function): T | Array; 20 | /** 21 | * Flatten the JSON API record so it can be inserted into the model 22 | * 23 | * @export 24 | * @param {IJsonApiRecord} record - original JSON API record 25 | * @returns {IDictionary} - Flattened object 26 | */ 27 | export declare function flattenRecord(record: JsonApi.IRecord): IDictionary; 28 | export declare const isBrowser: boolean; 29 | /** 30 | * Assign objects to the target object 31 | * Not a complete implementation (Object.assign) 32 | * Based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign polyfill 33 | * 34 | * @private 35 | * @param {object} target - Target object 36 | * @param {Array} args - Objects to be assigned 37 | * @returns 38 | */ 39 | export declare function assign(target: object, ...args: Array): object; 40 | /** 41 | * Returns the value if it's not a function. If it's a function 42 | * it calls it. 43 | * 44 | * @export 45 | * @template T 46 | * @param {(T|(() => T))} target can be anything or function 47 | * @returns {T} value 48 | */ 49 | export declare function getValue(target: T | (() => T)): T; 50 | /** 51 | * Get all object keys 52 | * 53 | * @export 54 | * @param {object} obj Object to process 55 | * @returns {Array} List of object keys 56 | */ 57 | export declare function keys(obj: object): Array; 58 | -------------------------------------------------------------------------------- /dist/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | /** 4 | * Iterate trough object keys 5 | * 6 | * @param {object} obj - Object that needs to be iterated 7 | * @param {Function} fn - Function that should be called for every iteration 8 | */ 9 | function objectForEach(obj, fn) { 10 | for (var key in obj) { 11 | /* istanbul ignore else */ 12 | if (obj.hasOwnProperty(key)) { 13 | fn(key); 14 | } 15 | } 16 | } 17 | exports.objectForEach = objectForEach; 18 | /** 19 | * Iterate trough one item or array of items and call the defined function 20 | * 21 | * @export 22 | * @template T 23 | * @param {(object|Array)} data - Data which needs to be iterated 24 | * @param {Function} fn - Function that needs to be callse 25 | * @returns {(T|Array)} - The result of iteration 26 | */ 27 | function mapItems(data, fn) { 28 | return data instanceof Array ? data.map(function (item) { return fn(item); }) : fn(data); 29 | } 30 | exports.mapItems = mapItems; 31 | /** 32 | * Flatten the JSON API record so it can be inserted into the model 33 | * 34 | * @export 35 | * @param {IJsonApiRecord} record - original JSON API record 36 | * @returns {IDictionary} - Flattened object 37 | */ 38 | function flattenRecord(record) { 39 | var data = { 40 | __internal: { 41 | id: record.id, 42 | type: record.type, 43 | }, 44 | }; 45 | objectForEach(record.attributes, function (key) { 46 | data[key] = record.attributes[key]; 47 | }); 48 | objectForEach(record.relationships, function (key) { 49 | var rel = record.relationships[key]; 50 | if (rel.meta) { 51 | data[key + "Meta"] = rel.meta; 52 | } 53 | if (rel.links) { 54 | data.__internal.relationships = data.__internal.relationships || {}; 55 | data.__internal.relationships[key] = rel.links; 56 | } 57 | }); 58 | objectForEach(record.links, function (key) { 59 | /* istanbul ignore else */ 60 | if (record.links[key]) { 61 | data.__internal.links = data.__internal.links || {}; 62 | data.__internal.links[key] = record.links[key]; 63 | } 64 | }); 65 | objectForEach(record.meta, function (key) { 66 | /* istanbul ignore else */ 67 | if (record.meta[key] !== undefined) { 68 | data.__internal.meta = data.__internal.meta || {}; 69 | data.__internal.meta[key] = record.meta[key]; 70 | } 71 | }); 72 | return data; 73 | } 74 | exports.flattenRecord = flattenRecord; 75 | exports.isBrowser = (typeof window !== 'undefined'); 76 | /** 77 | * Assign objects to the target object 78 | * Not a complete implementation (Object.assign) 79 | * Based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign polyfill 80 | * 81 | * @private 82 | * @param {object} target - Target object 83 | * @param {Array} args - Objects to be assigned 84 | * @returns 85 | */ 86 | function assign(target) { 87 | var args = []; 88 | for (var _i = 1; _i < arguments.length; _i++) { 89 | args[_i - 1] = arguments[_i]; 90 | } 91 | args.forEach(function (nextSource) { 92 | if (nextSource != null) { 93 | for (var nextKey in nextSource) { 94 | /* istanbul ignore else */ 95 | if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { 96 | target[nextKey] = nextSource[nextKey]; 97 | } 98 | } 99 | } 100 | }); 101 | return target; 102 | } 103 | exports.assign = assign; 104 | /** 105 | * Returns the value if it's not a function. If it's a function 106 | * it calls it. 107 | * 108 | * @export 109 | * @template T 110 | * @param {(T|(() => T))} target can be anything or function 111 | * @returns {T} value 112 | */ 113 | function getValue(target) { 114 | if (typeof target === 'function') { 115 | // @ts-ignore 116 | return target(); 117 | } 118 | return target; 119 | } 120 | exports.getValue = getValue; 121 | /** 122 | * Get all object keys 123 | * 124 | * @export 125 | * @param {object} obj Object to process 126 | * @returns {Array} List of object keys 127 | */ 128 | function keys(obj) { 129 | var keyList = []; 130 | for (var key in obj) { 131 | if (obj.hasOwnProperty(key)) { 132 | keyList.push(key); 133 | } 134 | } 135 | return keyList; 136 | } 137 | exports.keys = keys; 138 | -------------------------------------------------------------------------------- /jsonapi.md: -------------------------------------------------------------------------------- 1 | # JSON API support status 2 | 3 | [Milestone for the spec compliance](https://github.com/infinum/mobx-jsonapi-store/milestone/1) 4 | 5 | Based on the official v1.0 [specification](http://jsonapi.org/format/), here is the status of every feature: 6 | 7 | ## [Content Negotiation](http://jsonapi.org/format/#content-negotiation) 8 | 9 | ### [Client Responsibilities](http://jsonapi.org/format/#content-negotiation-clients) 10 | * The lib is already setting the required headers, but you can also extend/override them if needed ✅ 11 | 12 | ### [Server Responsibilities](http://jsonapi.org/format/#content-negotiation-servers) 13 | * If the server returns any HTTP status 400 or greater (including 406 and 415 mentioned in the spec), the request promise will reject. ✅ 14 | 15 | ## [Document Structure](http://jsonapi.org/format/#document-structure) 16 | 17 | ### [Top Level](http://jsonapi.org/format/#document-top-level) 18 | * The lib supports `data`, `errors`, `meta`, `links` and `included` properties. It will ignore `jsonapi` property for now. ✅ 19 | 20 | ### [Resource Objects](http://jsonapi.org/format/#document-resource-objects) 21 | * The lib expects `id` and `type` ✅ 22 | * If the resource has `attributes`, they will be added to the record ✅ 23 | * If the resource has `relationships` 24 | * `links` will be available using the `getRelationshipLinks()` method on the record ✅ 25 | * Use `fetchRelationshipLink(relationship, link)` to fetch the link ✅ 26 | * `data` will be used to build the references between models 27 | * If the store already contains the referenced model or `includes` contains the model it will be available right away on the record: `record[relName]`. Also, the id is available on `record[relName + 'Id']`. ✅ 28 | * If the model is "unknown", the id will be available on `record[relName + 'Id']`. ✅ 29 | * `meta` is available on `record[relName + 'Meta']` ✅ 30 | * If the resource has `links` or `meta`, they will be available with the `getLinks()` and `getMeta()` methods on the record ✅ 31 | * Use `fetchLink(link)` to fetch the link ✅ 32 | 33 | ### [Resource Identifier Objects](http://jsonapi.org/format/#document-resource-identifier-objects) 34 | * The lib expects `id` and `type` ✅ 35 | * `meta` is available on `record[relName + 'Meta']` ✅ 36 | 37 | ### [Compound Objects](http://jsonapi.org/format/#document-compound-documents) 38 | * The lib supports `included` property ✅ 39 | * The lib will add `included` resources as references to the `data` resources ✅ 40 | * The `included` resources are treated as regular records, just like the `data` resources, with same features and limitations ✅ 41 | 42 | ### [Meta Information](http://jsonapi.org/format/#document-meta) 43 | * The `meta` property will be available directly on the `Response` object ✅ 44 | 45 | ### [Links](http://jsonapi.org/format/#document-links) 46 | * The `links` property is supported, and exposes all links directly on the `Response` object ✅ 47 | * They are also available in raw form as a `links` property on the `Response` object ✅ 48 | * `response[linkName]` is a Promise that will resolve to the link content or reject with an error. In both cases, it will be a `Response` object ✅ 49 | * The link is lazily evaluated, so the request won't be made until you access the property 50 | * The link can be a string with an URL ✅ 51 | 52 | ### [JSON API Object](http://jsonapi.org/format/#document-jsonapi-object) 53 | * The `jsonapi` property is available on `response.jsonapi` ✅ 54 | 55 | ### [Member Names](http://jsonapi.org/format/#document-member-names) 56 | * All member names are treated as strings, therefore adhere to the specification ✅ 57 | * If a name is not a valid variable name, e.g. `"first-name"`, you can access it as `record['first-name']` ✅ 58 | 59 | ## [Fetching Data](http://jsonapi.org/format/#fetching) 60 | 61 | ### [Fetching Resources](http://jsonapi.org/format/#fetching-resources) 62 | * Fetch top-level `links` ✅ 63 | * Fetch resource-level `links` with `record.fetchLink(link)` ✅ 64 | * Fetch `related` link from relationship-level `links` with `record.fetchRelationshipLink(relationship, link)` ✅ 65 | 66 | ### [Fetching Relationships](http://jsonapi.org/format/#fetching-relationships) 67 | * Fetch relationship-level `links` with `record.fetchRelationshipLink(relationship, link)` ✅ 68 | 69 | ### [Inclusion of Related Resources](http://jsonapi.org/format/#fetching-includes) 70 | * Supported since version 3.4.0 [#15](https://github.com/infinum/mobx-jsonapi-store/issues/15) ✅ 71 | 72 | ### [Sparse Fieldsets](http://jsonapi.org/format/#fetching-sparse-fieldsets) 73 | * Supported since version 3.4.0 [#16](https://github.com/infinum/mobx-jsonapi-store/issues/16) ✅ 74 | 75 | ### [Sorting](http://jsonapi.org/format/#fetching-sorting) 76 | * Supported since version 3.4.0 [#17](https://github.com/infinum/mobx-jsonapi-store/issues/17) ✅ 77 | 78 | ### [Pagination](http://jsonapi.org/format/#fetching-pagination) 79 | * The lib supports top-level links ✅ 80 | * The lib allows access to raw links with `response.links` ✅ 81 | * The lib allows pagination with links, e.g. `response.next` will return a Promise that will resolve to the link content or reject with an error. In both cases, it will be a `Response` object ✅ 82 | * The link is lazily evaluated, so the request won't be made until you access the property 83 | 84 | ### [Filtering](http://jsonapi.org/format/#fetching-filtering) 85 | * Supported since version 3.4.0 [#18](https://github.com/infinum/mobx-jsonapi-store/issues/18) ✅ 86 | 87 | ## [Creating, Updating and Deleting Resources](http://jsonapi.org/format/#crud) 88 | 89 | ### [Creating Resources](http://jsonapi.org/format/#crud-creating) 90 | * Creating resources is supported using the `save()` method on the record if the record was created on the client ✅ 91 | * Client-Generated IDs are supported - just make sure you're using a valid UUID generator ✅ 92 | 93 | ### [Updating Resources](http://jsonapi.org/format/#crud-updating) 94 | * Updating resources is supported using the `save()` method on the record if the record was not created on the client ✅ 95 | 96 | ### [Updating Relationships](http://jsonapi.org/format/#crud-updating-relationships) 97 | * Direct update of relationships ✅ 98 | 99 | ### [Deleting Resources](http://jsonapi.org/format/#crud-deleting) 100 | * The resource can be deleted with the `remove()` method on the record ✅ 101 | 102 | ## [Query Parameters](http://jsonapi.org/format/#query-parameters) 103 | * All current communication with the server is using the required naming method ✅ 104 | 105 | ## [Errors](http://jsonapi.org/format/#errors) 106 | 107 | ### [Processing Errors](http://jsonapi.org/format/#errors-processing) 108 | * The lib will process all HTTP status 400+ as errors ✅ 109 | 110 | ### [Error Objects](http://jsonapi.org/format/#error-objects) 111 | * The error objects will be available in the `Response` object under the `error` property (without any modification) ✅ 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobx-jsonapi-store", 3 | "version": "4.4.0", 4 | "description": "JSON API Store for MobX", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc --outDir ./dist --experimentalDecorators --target es5 --module commonjs --declaration --skipLibCheck --lib 'dom','es5','scripthost','es2015.promise' --pretty src/index.ts", 9 | "test": "NODE_ENV=test nyc mocha", 10 | "lint": "tslint './src/**/*.ts'", 11 | "watch": "nodemon -e .ts -i node_modules -i dist -i coverage -x mocha" 12 | }, 13 | "husky": { 14 | "hooks": { 15 | "pre-commit": "npm run lint && npm test && npm run build && git add dist/" 16 | } 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/infinum/mobx-jsonapi-store.git" 21 | }, 22 | "keywords": [ 23 | "mobx", 24 | "jsonapi", 25 | "store", 26 | "observable" 27 | ], 28 | "bugs": { 29 | "url": "https://github.com/infinum/mobx-jsonapi-store/issues" 30 | }, 31 | "homepage": "https://github.com/infinum/mobx-jsonapi-store#readme", 32 | "author": "Infinum ", 33 | "contributors": [ 34 | { 35 | "name": "Darko Kukovec", 36 | "email": "darko@infinum.co" 37 | } 38 | ], 39 | "nyc": { 40 | "extension": [ 41 | ".ts" 42 | ], 43 | "require": [ 44 | "ts-node/register" 45 | ], 46 | "exclude": [ 47 | "src/interfaces/*", 48 | "test/*" 49 | ], 50 | "sourceMap": true, 51 | "instrument": true, 52 | "cache": false, 53 | "reporter": [ 54 | "lcov", 55 | "text", 56 | "text-summary" 57 | ] 58 | }, 59 | "license": "MIT", 60 | "dependencies": { 61 | "mobx-collection-store": "^2.0.0" 62 | }, 63 | "devDependencies": { 64 | "@types/mocha": "^5.2.6", 65 | "@types/nock": "^11.1.0", 66 | "@types/node": "^12.0.0", 67 | "chai": "^4.2.0", 68 | "husky": "^3.0.0", 69 | "isomorphic-fetch": "^2.2.1", 70 | "lodash": "^4.17.11", 71 | "mobx": "^3.6.2", 72 | "mocha": "^6.0.2", 73 | "nock": "^10.0.6", 74 | "nodemon": "^1.18.10", 75 | "nyc": "^14.0.0", 76 | "ts-node": "^8.0.2", 77 | "tslint": "^5.13.1", 78 | "typescript": "^3.3.3333" 79 | }, 80 | "peerDependencies": { 81 | "mobx": "^3.0.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/NetworkStore.ts: -------------------------------------------------------------------------------- 1 | import {Collection, IModelConstructor} from 'mobx-collection-store'; 2 | 3 | import IDictionary from './interfaces/IDictionary'; 4 | import IHeaders from './interfaces/IHeaders'; 5 | import IRequestOptions from './interfaces/IRequestOptions'; 6 | import * as JsonApi from './interfaces/JsonApi'; 7 | import {buildUrl} from './NetworkUtils'; 8 | 9 | export class NetworkStore extends Collection { 10 | 11 | /** 12 | * Prepare the query params for the API call 13 | * 14 | * @protected 15 | * @param {string} type Record type 16 | * @param {(number|string)} [id] Record ID 17 | * @param {JsonApi.IRequest} [data] Request data 18 | * @param {IRequestOptions} [options] Server options 19 | * @returns {{ 20 | * url: string, 21 | * data?: object, 22 | * headers: IHeaders, 23 | * }} Options needed for an API call 24 | * 25 | * @memberOf NetworkStore 26 | */ 27 | protected __prepareQuery( 28 | type: string, 29 | id?: number|string, 30 | data?: JsonApi.IRequest, 31 | options?: IRequestOptions, 32 | ): { 33 | url: string, 34 | data?: object, 35 | headers: IHeaders, 36 | } { 37 | const model: IModelConstructor = this.static.types.filter((item) => item.type === type)[0]; 38 | const headers: IDictionary = (options ? options.headers : {}) || {}; 39 | 40 | const url = buildUrl(type, id, model, options); 41 | return {data, headers, url}; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/NetworkUtils.ts: -------------------------------------------------------------------------------- 1 | import {IModelConstructor} from 'mobx-collection-store'; 2 | 3 | import ParamArrayType from './enums/ParamArrayType'; 4 | import IDictionary from './interfaces/IDictionary'; 5 | import IFilters from './interfaces/IFilters'; 6 | import IHeaders from './interfaces/IHeaders'; 7 | import IRawResponse from './interfaces/IRawResponse'; 8 | import IRequestOptions from './interfaces/IRequestOptions'; 9 | import IResponseHeaders from './interfaces/IResponseHeaders'; 10 | import * as JsonApi from './interfaces/JsonApi'; 11 | 12 | import {Record} from './Record'; 13 | import {Response as LibResponse} from './Response'; 14 | import {Store} from './Store'; 15 | import {assign, getValue, isBrowser, objectForEach} from './utils'; 16 | 17 | export type FetchType = ( 18 | method: string, 19 | url: string, 20 | body?: object, 21 | requestHeaders?: IHeaders, 22 | ) => Promise; 23 | 24 | export interface IStoreFetchOpts { 25 | url: string; 26 | options?: IRequestOptions; 27 | data?: object; 28 | method: string; 29 | store: Store; 30 | } 31 | 32 | export type StoreFetchType = (options: IStoreFetchOpts) => Promise; 33 | 34 | export interface IConfigType { 35 | baseFetch: FetchType; 36 | baseUrl: string; 37 | defaultHeaders: IHeaders; 38 | defaultFetchOptions: IDictionary; 39 | fetchReference: Function; 40 | paramArrayType: ParamArrayType; 41 | storeFetch: StoreFetchType; 42 | transformRequest: (options: IStoreFetchOpts) => IStoreFetchOpts; 43 | transformResponse: (response: IRawResponse) => IRawResponse; 44 | } 45 | 46 | export const config: IConfigType = { 47 | 48 | /** Base URL for all API calls */ 49 | baseUrl: '/', 50 | 51 | /** Default headers that will be sent to the server */ 52 | defaultHeaders: { 53 | 'content-type': 'application/vnd.api+json', 54 | }, 55 | 56 | /* Default options that will be passed to fetchReference */ 57 | defaultFetchOptions: {}, 58 | 59 | /** Reference of the fetch method that should be used */ 60 | /* istanbul ignore next */ 61 | fetchReference: isBrowser && window.fetch && window.fetch.bind(window), 62 | 63 | /** Determines how will the request param arrays be stringified */ 64 | paramArrayType: ParamArrayType.COMMA_SEPARATED, // As recommended by the spec 65 | 66 | /** 67 | * Base implementation of the fetch function (can be overridden) 68 | * 69 | * @param {string} method API call method 70 | * @param {string} url API call URL 71 | * @param {object} [body] API call body 72 | * @param {IHeaders} [requestHeaders] Headers that will be sent 73 | * @returns {Promise} Resolves with a raw response object 74 | */ 75 | baseFetch( 76 | method: string, 77 | url: string, 78 | body?: object, 79 | requestHeaders?: IHeaders, 80 | ): Promise { 81 | let data: JsonApi.IResponse; 82 | let status: number; 83 | let headers: IResponseHeaders; 84 | 85 | const request: Promise = Promise.resolve(); 86 | 87 | const uppercaseMethod = method.toUpperCase(); 88 | const isBodySupported = uppercaseMethod !== 'GET' && uppercaseMethod !== 'HEAD'; 89 | 90 | return request 91 | .then(() => { 92 | const reqHeaders: IHeaders = assign({}, config.defaultHeaders, requestHeaders) as IHeaders; 93 | const options = assign({}, config.defaultFetchOptions, { 94 | body: isBodySupported && JSON.stringify(body) || undefined, 95 | headers: reqHeaders, 96 | method, 97 | }); 98 | return this.fetchReference(url, options); 99 | }) 100 | .then((response: Response) => { 101 | status = response.status; 102 | headers = response.headers; 103 | return response.json(); 104 | }) 105 | .catch((e: Error) => { 106 | if (status === 204) { 107 | return null; 108 | } 109 | throw e; 110 | }) 111 | .then((responseData: JsonApi.IResponse) => { 112 | data = responseData; 113 | if (status >= 400) { 114 | throw { 115 | message: `Invalid HTTP status: ${status}`, 116 | status, 117 | }; 118 | } 119 | 120 | return {data, headers, requestHeaders, status}; 121 | }) 122 | .catch((error) => { 123 | return {data, error, headers, requestHeaders, status}; 124 | }); 125 | }, 126 | /** 127 | * Base implementation of the stateful fetch function (can be overridden) 128 | * 129 | * @param {IStoreFetchOpts} reqOptions API request options 130 | * @returns {Promise} Resolves with a response object 131 | */ 132 | storeFetch(reqOptions: IStoreFetchOpts): Promise { 133 | const { 134 | url, 135 | options, 136 | data, 137 | method = 'GET', 138 | store, 139 | } = config.transformRequest(reqOptions); 140 | 141 | return config.baseFetch(method, url, data, options && options.headers) 142 | .then((response: IRawResponse) => { 143 | const storeResponse = assign(response, {store}); 144 | return new LibResponse(config.transformResponse(storeResponse), store, options); 145 | }); 146 | }, 147 | 148 | transformRequest(options: IStoreFetchOpts): IStoreFetchOpts { 149 | return options; 150 | }, 151 | 152 | transformResponse(response: IRawResponse): IRawResponse { 153 | return response; 154 | }, 155 | }; 156 | 157 | export function fetch(options: IStoreFetchOpts) { 158 | return config.storeFetch(options); 159 | } 160 | 161 | /** 162 | * API call used to get data from the server 163 | * 164 | * @export 165 | * @param {Store} store Related Store 166 | * @param {string} url API call URL 167 | * @param {IHeaders} [headers] Headers to be sent 168 | * @param {IRequestOptions} [options] Server options 169 | * @returns {Promise} Resolves with a Response object 170 | */ 171 | export function read( 172 | store: Store, 173 | url: string, 174 | headers?: IHeaders, 175 | options?: IRequestOptions, 176 | ): Promise { 177 | return config.storeFetch({ 178 | data: null, 179 | method: 'GET', 180 | options: {...options, headers}, 181 | store, 182 | url, 183 | }); 184 | } 185 | 186 | /** 187 | * API call used to create data on the server 188 | * 189 | * @export 190 | * @param {Store} store Related Store 191 | * @param {string} url API call URL 192 | * @param {object} [data] Request body 193 | * @param {IHeaders} [headers] Headers to be sent 194 | * @param {IRequestOptions} [options] Server options 195 | * @returns {Promise} Resolves with a Response object 196 | */ 197 | export function create( 198 | store: Store, 199 | url: string, 200 | data?: object, 201 | headers?: IHeaders, 202 | options?: IRequestOptions, 203 | ): Promise { 204 | return config.storeFetch({ 205 | data, 206 | method: 'POST', 207 | options: {...options, headers}, 208 | store, 209 | url, 210 | }); 211 | } 212 | 213 | /** 214 | * API call used to update data on the server 215 | * 216 | * @export 217 | * @param {Store} store Related Store 218 | * @param {string} url API call URL 219 | * @param {object} [data] Request body 220 | * @param {IHeaders} [headers] Headers to be sent 221 | * @param {IRequestOptions} [options] Server options 222 | * @returns {Promise} Resolves with a Response object 223 | */ 224 | export function update( 225 | store: Store, 226 | url: string, 227 | data?: object, 228 | headers?: IHeaders, 229 | options?: IRequestOptions, 230 | ): Promise { 231 | return config.storeFetch({ 232 | data, 233 | method: 'PATCH', 234 | options: {...options, headers}, 235 | store, 236 | url, 237 | }); 238 | } 239 | 240 | /** 241 | * API call used to remove data from the server 242 | * 243 | * @export 244 | * @param {Store} store Related Store 245 | * @param {string} url API call URL 246 | * @param {IHeaders} [headers] Headers to be sent 247 | * @param {IRequestOptions} [options] Server options 248 | * @returns {Promise} Resolves with a Response object 249 | */ 250 | export function remove( 251 | store: Store, 252 | url: string, 253 | headers?: IHeaders, 254 | options?: IRequestOptions, 255 | ): Promise { 256 | return config.storeFetch({ 257 | data: null, 258 | method: 'DELETE', 259 | options: {...options, headers}, 260 | store, 261 | url, 262 | }); 263 | } 264 | 265 | /** 266 | * Fetch a link from the server 267 | * 268 | * @export 269 | * @param {JsonApi.ILink} link Link URL or a link object 270 | * @param {Store} store Store that will be used to save the response 271 | * @param {IDictionary} [requestHeaders] Request headers 272 | * @param {IRequestOptions} [options] Server options 273 | * @returns {Promise} Response promise 274 | */ 275 | export function fetchLink( 276 | link: JsonApi.ILink, 277 | store: Store, 278 | requestHeaders?: IDictionary, 279 | options?: IRequestOptions, 280 | ): Promise { 281 | if (link) { 282 | const href: string = typeof link === 'object' ? link.href : link; 283 | 284 | /* istanbul ignore else */ 285 | if (href) { 286 | return read(store, href, requestHeaders, options); 287 | } 288 | } 289 | return Promise.resolve(new LibResponse({data: null}, store)); 290 | } 291 | 292 | export function handleResponse(record: Record, prop?: string): (response: LibResponse) => Record { 293 | return (response: LibResponse): Record => { 294 | 295 | /* istanbul ignore if */ 296 | if (response.error) { 297 | throw response.error; 298 | } 299 | 300 | if (response.status === 204) { 301 | record['__persisted'] = true; 302 | return record as Record; 303 | } else if (response.status === 202) { 304 | (response.data as Record).update({ 305 | __prop__: prop, 306 | __queue__: true, 307 | __related__: record, 308 | } as Object); 309 | return response.data as Record; 310 | } else { 311 | record['__persisted'] = true; 312 | return response.replaceData(record).data as Record; 313 | } 314 | }; 315 | } 316 | 317 | function __prepareFilters(filters: IFilters): Array { 318 | return __parametrize(filters).map((item) => `filter[${item.key}]=${item.value}`); 319 | } 320 | 321 | function __prepareSort(sort?: string|Array): Array { 322 | return sort ? [`sort=${sort}`] : []; 323 | } 324 | 325 | function __prepareIncludes(include?: string|Array): Array { 326 | return include ? [`include=${include}`] : []; 327 | } 328 | 329 | function __prepareFields(fields: IDictionary>): Array { 330 | const list = []; 331 | 332 | objectForEach(fields, (key: string) => { 333 | list.push(`fields[${key}]=${fields[key]}`); 334 | }); 335 | 336 | return list; 337 | } 338 | 339 | function __prepareRawParams(params: Array<{key: string, value: string}|string>): Array { 340 | return params.map((param) => { 341 | if (typeof param === 'string') { 342 | return param; 343 | } 344 | return `${param.key}=${param.value}`; 345 | }); 346 | } 347 | 348 | export function prefixUrl(url) { 349 | return `${config.baseUrl}${url}`; 350 | } 351 | 352 | function __appendParams(url: string, params: Array): string { 353 | if (params.length) { 354 | url += '?' + params.join('&'); 355 | } 356 | return url; 357 | } 358 | 359 | function __parametrize(params: object, scope: string = ''): Array<{key: string, value: string}> { 360 | const list = []; 361 | 362 | objectForEach(params, (key: string) => { 363 | if (params[key] instanceof Array) { 364 | if (config.paramArrayType === ParamArrayType.OBJECT_PATH) { 365 | list.push(...__parametrize(params[key], `${key}.`)); 366 | } else if (config.paramArrayType === ParamArrayType.COMMA_SEPARATED) { 367 | list.push({key: `${scope}${key}`, value: params[key].join(',')}); 368 | } else if (config.paramArrayType === ParamArrayType.MULTIPLE_PARAMS) { 369 | list.push(...params[key].map((param) => ({key: `${scope}${key}`, value: param}))); 370 | } else if (config.paramArrayType === ParamArrayType.PARAM_ARRAY) { 371 | list.push(...params[key].map((param) => ({key: `${scope}${key}][`, value: param}))); 372 | } 373 | } else if (typeof params[key] === 'object') { 374 | list.push(...__parametrize(params[key], `${key}.`)); 375 | } else { 376 | list.push({key: `${scope}${key}`, value: params[key]}); 377 | } 378 | }); 379 | 380 | return list; 381 | } 382 | 383 | export function buildUrl( 384 | type: number|string, 385 | id?: number|string, 386 | model?: IModelConstructor, 387 | options?: IRequestOptions, 388 | ) { 389 | const path: string = model 390 | ? (getValue(model['endpoint']) || model['baseUrl'] || model.type) 391 | : type; 392 | 393 | const url: string = id ? `${path}/${id}` : `${path}`; 394 | 395 | const params: Array = [ 396 | ...__prepareFilters((options && options.filter) || {}), 397 | ...__prepareSort(options && options.sort), 398 | ...__prepareIncludes(options && options.include), 399 | ...__prepareFields((options && options.fields) || {}), 400 | ...__prepareRawParams((options && options.params) || []), 401 | ]; 402 | 403 | return __appendParams(prefixUrl(url), params); 404 | } 405 | -------------------------------------------------------------------------------- /src/Record.ts: -------------------------------------------------------------------------------- 1 | import {IModel, Model} from 'mobx-collection-store'; 2 | 3 | import IDictionary from './interfaces/IDictionary'; 4 | import IRequestOptions from './interfaces/IRequestOptions'; 5 | import * as JsonApi from './interfaces/JsonApi'; 6 | 7 | import {buildUrl, create, fetchLink, handleResponse, remove, update} from './NetworkUtils'; 8 | import {Response} from './Response'; 9 | import {Store} from './Store'; 10 | import {getValue, mapItems, objectForEach} from './utils'; 11 | 12 | interface IInternal { 13 | relationships?: IDictionary; 14 | meta?: object; 15 | links?: IDictionary; 16 | persisted?: boolean; 17 | id: number|string; 18 | type: string; 19 | } 20 | 21 | export class Record extends Model implements IModel { 22 | 23 | /** 24 | * Type property of the record class 25 | * 26 | * @static 27 | * 28 | * @memberOf Record 29 | */ 30 | public static typeAttribute = ['__internal', 'type']; 31 | 32 | /** 33 | * ID property of the record class 34 | * 35 | * @static 36 | * 37 | * @memberOf Record 38 | */ 39 | public static idAttribute = ['__internal', 'id']; 40 | 41 | /** 42 | * Should the autogenerated ID be sent to the server when creating a record 43 | * 44 | * @static 45 | * @type {boolean} 46 | * @memberOf Record 47 | */ 48 | public static useAutogeneratedIds: boolean = false; 49 | 50 | /** 51 | * Endpoint for API requests if there is no self link 52 | * 53 | * @static 54 | * @type {string|() => string} 55 | * @memberOf Record 56 | */ 57 | public static endpoint: string|(() => string); 58 | 59 | public 'static': typeof Record; 60 | 61 | /** 62 | * Internal metadata 63 | * 64 | * @private 65 | * @type {IInternal} 66 | * @memberOf Record 67 | */ 68 | private __internal: IInternal; 69 | 70 | /** 71 | * Cache link fetch requests 72 | * 73 | * @private 74 | * @type {IDictionary>} 75 | * @memberOf Record 76 | */ 77 | private __relationshipLinkCache: IDictionary>> = {}; 78 | 79 | /** 80 | * Cache link fetch requests 81 | * 82 | * @private 83 | * @type {IDictionary>} 84 | * @memberOf Record 85 | */ 86 | private __linkCache: IDictionary> = {}; 87 | 88 | /** 89 | * Get record relationship links 90 | * 91 | * @returns {IDictionary} Record relationship links 92 | * 93 | * @memberOf Record 94 | */ 95 | public getRelationshipLinks(): IDictionary { 96 | return this.__internal && this.__internal.relationships; 97 | } 98 | 99 | /** 100 | * Fetch a relationship link 101 | * 102 | * @param {string} relationship Name of the relationship 103 | * @param {string} name Name of the link 104 | * @param {IRequestOptions} [options] Server options 105 | * @param {boolean} [force=false] Ignore the existing cache 106 | * @returns {Promise} Response promise 107 | * 108 | * @memberOf Record 109 | */ 110 | public fetchRelationshipLink( 111 | relationship: string, 112 | name: string, 113 | options?: IRequestOptions, 114 | force: boolean = false, 115 | ): Promise { 116 | this.__relationshipLinkCache[relationship] = this.__relationshipLinkCache[relationship] || {}; 117 | 118 | /* istanbul ignore else */ 119 | if (!(name in this.__relationshipLinkCache) || force) { 120 | const link: JsonApi.ILink = ( 121 | 'relationships' in this.__internal && 122 | relationship in this.__internal.relationships && 123 | name in this.__internal.relationships[relationship] 124 | ) ? this.__internal.relationships[relationship][name] : null; 125 | const headers: IDictionary = options && options.headers; 126 | 127 | this.__relationshipLinkCache[relationship][name] = fetchLink(link, this.__collection as Store, headers, options); 128 | } 129 | 130 | return this.__relationshipLinkCache[relationship][name]; 131 | } 132 | 133 | /** 134 | * Get record metadata 135 | * 136 | * @returns {object} Record metadata 137 | * 138 | * @memberOf Record 139 | */ 140 | public getMeta(): object { 141 | return this.__internal && this.__internal.meta; 142 | } 143 | 144 | /** 145 | * Get record links 146 | * 147 | * @returns {IDictionary} Record links 148 | * 149 | * @memberOf Record 150 | */ 151 | public getLinks(): IDictionary { 152 | return this.__internal && this.__internal.links; 153 | } 154 | 155 | /** 156 | * Fetch a record link 157 | * 158 | * @param {string} name Name of the link 159 | * @param {IRequestOptions} [options] Server options 160 | * @param {boolean} [force=false] Ignore the existing cache 161 | * @returns {Promise} Response promise 162 | * 163 | * @memberOf Record 164 | */ 165 | public fetchLink(name: string, options?: IRequestOptions, force: boolean = false): Promise { 166 | if (!(name in this.__linkCache) || force) { 167 | const link: JsonApi.ILink = ('links' in this.__internal && name in this.__internal.links) ? 168 | this.__internal.links[name] : null; 169 | this.__linkCache[name] = fetchLink(link, this.__collection as Store, options && options.headers, options); 170 | } 171 | 172 | let request: Promise = this.__linkCache[name]; 173 | 174 | if (this['__queue__']) { 175 | request = this.__linkCache[name].then((response) => { 176 | const related: Record = this['__related__']; 177 | const prop: string = this['__prop__']; 178 | const record: Record = response.data as Record; 179 | if (record && 180 | record.getRecordType() !== this.getRecordType() && 181 | record.getRecordType() === related.getRecordType() 182 | ) { 183 | 184 | /* istanbul ignore if */ 185 | if (prop) { 186 | related[prop] = record; 187 | return response; 188 | } 189 | related.__persisted = true; 190 | return response.replaceData(related); 191 | } 192 | return response; 193 | }); 194 | } 195 | 196 | return request; 197 | } 198 | 199 | /** 200 | * Get the persisted state 201 | * 202 | * @readonly 203 | * @private 204 | * @type {boolean} 205 | * @memberOf Record 206 | */ 207 | private get __persisted(): boolean { 208 | return (this.__internal && this.__internal.persisted) || false; 209 | } 210 | 211 | /** 212 | * Set the persisted state 213 | * 214 | * @private 215 | * 216 | * @memberOf Record 217 | */ 218 | private set __persisted(state: boolean) { 219 | this.__internal.persisted = state; 220 | } 221 | 222 | /** 223 | * Serialize the record into JSON API format 224 | * 225 | * @returns {JsonApi.IRecord} JSON API formated record 226 | * 227 | * @memberOf Record 228 | */ 229 | public toJsonApi(): JsonApi.IRecord { 230 | const attributes: IDictionary = this.toJS(); 231 | 232 | const useAutogenerated: boolean = this.static['useAutogeneratedIds']; 233 | const data: JsonApi.IRecord = { 234 | attributes, 235 | id: (this.__persisted || useAutogenerated) ? this.getRecordId() : undefined, 236 | type: this.getRecordType() as string, 237 | }; 238 | 239 | const refs: IDictionary = this['__refs']; 240 | objectForEach(refs, (key: string) => { 241 | data.relationships = data.relationships || {}; 242 | const rel = mapItems(this[`${key}Id`], (id: number|string) => { 243 | if (!id && id !== 0) { 244 | return null; 245 | } 246 | return {id, type: refs[key]}; 247 | }); 248 | data.relationships[key] = {data: rel} as JsonApi.IRelationship; 249 | 250 | delete data.attributes[key]; 251 | delete data.attributes[`${key}Id`]; 252 | delete data.attributes[`${key}Meta`]; 253 | }); 254 | 255 | delete data.attributes.__internal; 256 | delete data.attributes.__type__; 257 | 258 | return data; 259 | } 260 | 261 | /** 262 | * Saves (creates or updates) the record to the server 263 | * 264 | * @param {IRequestOptions} [options] Server options 265 | * @param {boolean} [ignoreSelf=false] Should the self link be ignored if it exists 266 | * @returns {Promise} Returns the record is successful or rejects with an error 267 | * 268 | * @memberOf Record 269 | */ 270 | public save(options?: IRequestOptions, ignoreSelf: boolean = false): Promise { 271 | const store: Store = this.__collection as Store; 272 | const data: JsonApi.IRecord = this.toJsonApi(); 273 | const requestMethod: Function = this.__persisted ? update : create; 274 | return requestMethod(store, this.__getUrl(options, ignoreSelf), {data}, options && options.headers) 275 | .then(handleResponse(this)); 276 | } 277 | 278 | public saveRelationship(relationship: string, options?: IRequestOptions): Promise { 279 | const link: JsonApi.ILink = ( 280 | 'relationships' in this.__internal && 281 | relationship in this.__internal.relationships && 282 | 'self' in this.__internal.relationships[relationship] 283 | ) ? this.__internal.relationships[relationship]['self'] : null; 284 | 285 | /* istanbul ignore if */ 286 | if (!link) { 287 | throw new Error('The relationship doesn\'t have a defined link'); 288 | } 289 | 290 | const store: Store = this.__collection as Store; 291 | 292 | /* istanbul ignore next */ 293 | const href: string = typeof link === 'object' ? link.href : link; 294 | 295 | const type: string = this['__refs'][relationship]; 296 | type ID = JsonApi.IIdentifier|Array; 297 | const data: ID = mapItems(this[`${relationship}Id`], (id) => ({id, type})) as ID; 298 | 299 | return update(store, href, {data}, options && options.headers) 300 | .then(handleResponse(this, relationship)); 301 | } 302 | 303 | /** 304 | * Remove the records from the server and store 305 | * 306 | * @param {IRequestOptions} [options] Server options 307 | * @param {boolean} [ignoreSelf=false] Should the self link be ignored if it exists 308 | * @returns {Promise} Resolves true if successfull or rejects if there was an error 309 | * 310 | * @memberOf Record 311 | */ 312 | public remove(options?: IRequestOptions, ignoreSelf: boolean = false): Promise { 313 | const store: Store = this.__collection as Store; 314 | if (!this.__persisted) { 315 | this.__collection.remove(this.getRecordType(), this.getRecordId()); 316 | return Promise.resolve(true); 317 | } 318 | return remove(store, this.__getUrl(options, ignoreSelf), options && options.headers) 319 | .then((response: Response) => { 320 | 321 | /* istanbul ignore if */ 322 | if (response.error) { 323 | throw response.error; 324 | } 325 | 326 | this.__persisted = false; 327 | 328 | if (this.__collection) { 329 | this.__collection.remove(this.getRecordType(), this.getRecordId()); 330 | } 331 | 332 | return true; 333 | }); 334 | } 335 | 336 | /** 337 | * Set the persisted status of the record 338 | * 339 | * @param {boolean} state Is the record persisted on the server 340 | * 341 | * @memberOf Record 342 | */ 343 | public setPersisted(state: boolean): void { 344 | this.__persisted = state; 345 | } 346 | 347 | /** 348 | * Get the persisted status of the record 349 | * 350 | * @memberOf Record 351 | */ 352 | public getPersisted(): boolean { 353 | return this.__persisted; 354 | } 355 | 356 | /** 357 | * Get the URL that should be used for the API calls 358 | * 359 | * @private 360 | * @returns {string} API URL 361 | * 362 | * @memberOf Record 363 | */ 364 | private __getUrl(options?: IRequestOptions, ignoreSelf?: boolean): string { 365 | 366 | const links: IDictionary = this.getLinks(); 367 | if (!ignoreSelf && links && links.self) { 368 | const self: JsonApi.ILink = links.self; 369 | 370 | /* istanbul ignore next */ 371 | return typeof self === 'string' ? self : self.href; 372 | } 373 | 374 | /* istanbul ignore next */ 375 | const type = getValue(this.static.endpoint) || this.getRecordType() || this.static.type; 376 | 377 | return buildUrl(type, this.__persisted ? this.getRecordId() : null, null, options); 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /src/Response.ts: -------------------------------------------------------------------------------- 1 | import {action, computed, extendObservable, IComputedValue, isObservableArray} from 'mobx'; 2 | import {IModel} from 'mobx-collection-store'; 3 | 4 | import IDictionary from './interfaces/IDictionary'; 5 | import IHeaders from './interfaces/IHeaders'; 6 | import IRawResponse from './interfaces/IRawResponse'; 7 | import IRequestOptions from './interfaces/IRequestOptions'; 8 | import IResponseHeaders from './interfaces/IResponseHeaders'; 9 | import * as JsonApi from './interfaces/JsonApi'; 10 | 11 | import {NetworkStore} from './NetworkStore'; 12 | import {Record} from './Record'; 13 | import {Store} from './Store'; 14 | import {flattenRecord, keys} from './utils'; 15 | 16 | import {fetchLink, read} from './NetworkUtils'; 17 | 18 | export class Response { 19 | 20 | /** 21 | * API response data (synced with the store) 22 | * 23 | * @type {(IModel|Array)} 24 | * @memberOf Response 25 | */ 26 | public data?: IModel|Array; 27 | 28 | /** 29 | * API response metadata 30 | * 31 | * @type {object} 32 | * @memberOf Response 33 | */ 34 | public meta?: object; 35 | 36 | /** 37 | * API reslonse links 38 | * 39 | * @type {object} 40 | * @memberOf Response 41 | */ 42 | public links?: IDictionary; 43 | 44 | /** 45 | * The JSON API object returned by the server 46 | * 47 | * @type {JsonApi.IJsonApiObject} 48 | * @memberOf Response 49 | */ 50 | public jsonapi?: JsonApi.IJsonApiObject; 51 | 52 | /** 53 | * Headers received from the API call 54 | * 55 | * @type {IResponseHeaders} 56 | * @memberOf Response 57 | */ 58 | public headers?: IResponseHeaders; 59 | 60 | /** 61 | * Headers sent to the server 62 | * 63 | * @type {IHeaders} 64 | * @memberOf Response 65 | */ 66 | public requestHeaders?: IHeaders; 67 | 68 | /** 69 | * Request error 70 | * 71 | * @type {(Array|Error)} 72 | * @memberOf Response 73 | */ 74 | public error?: Array|Error; 75 | 76 | /** 77 | * First data page 78 | * 79 | * @type {Promise} 80 | * @memberOf Response 81 | */ 82 | public first: Promise; // Handled by the __fetchLink 83 | 84 | /** 85 | * Previous data page 86 | * 87 | * @type {Promise} 88 | * @memberOf Response 89 | */ 90 | public prev: Promise; // Handled by the __fetchLink 91 | 92 | /** 93 | * Next data page 94 | * 95 | * @type {Promise} 96 | * @memberOf Response 97 | */ 98 | public next: Promise; // Handled by the __fetchLink 99 | 100 | /** 101 | * Last data page 102 | * 103 | * @type {Promise} 104 | * @memberOf Response 105 | */ 106 | public last: Promise; // Handled by the __fetchLink 107 | 108 | /** 109 | * Received HTTP status 110 | * 111 | * @type {number} 112 | * @memberOf Response 113 | */ 114 | public status: number; 115 | 116 | /** 117 | * Related Store 118 | * 119 | * @private 120 | * @type {Store} 121 | * @memberOf Response 122 | */ 123 | private __store: Store; 124 | 125 | /** 126 | * Server options 127 | * 128 | * @private 129 | * @type {IRequestOptions} 130 | * @memberOf Response 131 | */ 132 | private __options: IRequestOptions; 133 | 134 | /** 135 | * Original server response 136 | * 137 | * @private 138 | * @type {IRawResponse} 139 | * @memberOf Response 140 | */ 141 | private __response: IRawResponse; 142 | 143 | /** 144 | * Cache used for the link requests 145 | * 146 | * @private 147 | * @type {IDictionary>} 148 | * @memberOf Response 149 | */ 150 | private __cache: IDictionary> = {}; 151 | 152 | constructor(response: IRawResponse, store?: Store, options?: IRequestOptions, overrideData?: IModel|Array) { 153 | this.__store = store; 154 | this.__options = options; 155 | this.__response = response; 156 | this.status = response.status; 157 | 158 | if (store) { 159 | this.data = overrideData ? store.add(overrideData) : store.sync(response.data); 160 | } else if (response.data) { 161 | // The case when a record is not in a store and save/remove are used 162 | const resp = response.data; 163 | 164 | /* istanbul ignore if */ 165 | if (resp.data instanceof Array) { 166 | throw new Error('A save/remove operation should not return an array of results'); 167 | } 168 | 169 | this.data = overrideData || new Record(flattenRecord(resp.data)); 170 | } 171 | 172 | this.meta = (response.data && response.data.meta) || {}; 173 | this.links = (response.data && response.data.links) || {}; 174 | this.jsonapi = (response.data && response.data.jsonapi) || {}; 175 | this.headers = response.headers; 176 | this.requestHeaders = response.requestHeaders; 177 | this.error = (response.data && response.data.errors) || response.error; 178 | 179 | const linkGetter: IDictionary>> = {}; 180 | Object.keys(this.links).forEach((link: string) => { 181 | linkGetter[link] = computed(() => this.__fetchLink(link)); 182 | }); 183 | 184 | extendObservable(this, linkGetter); 185 | 186 | Object.freeze(this); 187 | 188 | if (this.error) { 189 | throw this; 190 | } 191 | } 192 | 193 | /** 194 | * Replace the response record with a different record. Used to replace a record while keeping the same reference 195 | * 196 | * @param {IModel} data New data 197 | * @returns {Response} 198 | * 199 | * @memberOf Response 200 | */ 201 | @action public replaceData(data: Record): Response { 202 | const record: Record = this.data as Record; 203 | if (record === data) { 204 | return this; 205 | } 206 | 207 | const oldId = data.getRecordId(); 208 | const newId = record.getRecordId(); 209 | const type = record.getRecordType(); 210 | 211 | if (this.__store) { 212 | this.__store.remove(type, newId); 213 | } 214 | 215 | data.update(record.toJS()); 216 | 217 | // TODO: Refactor this to avoid using mobx-collection-store internals 218 | data['__internal'].id = newId; 219 | this.__updateStoreReferences(type, oldId, newId); 220 | 221 | return new Response(this.__response, this.__store, this.__options, data); 222 | } 223 | 224 | /** 225 | * Update references in the store 226 | * 227 | * @private 228 | * @param {any} type Record type 229 | * @param {any} oldId Old redord ID 230 | * @param {any} newId New record ID 231 | * @memberof Response 232 | */ 233 | private __updateStoreReferences(type, oldId, newId) { 234 | if (this.__store) { 235 | const modelHash = this.__store['__modelHash'][type]; 236 | const oldModel = modelHash[oldId]; 237 | modelHash[newId] = oldModel; 238 | delete modelHash[oldId]; 239 | 240 | this.__updateReferences(type, oldId, newId); 241 | } 242 | } 243 | 244 | /** 245 | * Update models that reference the updated model 246 | * 247 | * @private 248 | * @param {any} oldId Old record ID 249 | * @param {any} newId new record ID 250 | * @memberof Response 251 | */ 252 | private __updateReferences(type, oldId, newId) { 253 | this.__store['__data'].map((model) => { 254 | const keyList = keys(model['__data']); 255 | keyList.map((key) => { 256 | const keyId = `${key}Id`; 257 | const refs = model.__refs || model.static.refs; 258 | const refsType = refs && refs[key]; 259 | if (key in model && keyId in model && refsType === type) { 260 | if (isObservableArray(model[keyId])) { 261 | const index = model[keyId].indexOf(oldId); 262 | if (index > -1) { 263 | model[keyId][index] = newId; 264 | } 265 | } else if (model[keyId] === oldId) { 266 | model[keyId] = newId; 267 | } 268 | } 269 | }); 270 | }); 271 | } 272 | 273 | /** 274 | * Function called when a link is beeing fetched. The returned value is cached 275 | * 276 | * @private 277 | * @param {any} name Link name 278 | * @returns Promise that resolves with a Response object 279 | * 280 | * @memberOf Response 281 | */ 282 | private __fetchLink(name) { 283 | if (!this.__cache[name]) { 284 | 285 | /* istanbul ignore next */ 286 | const link: JsonApi.ILink = name in this.links ? this.links[name] : null; 287 | 288 | this.__cache[name] = fetchLink(link, this.__store, this.requestHeaders, this.__options); 289 | } 290 | return this.__cache[name]; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/Store.ts: -------------------------------------------------------------------------------- 1 | import {action} from 'mobx'; 2 | 3 | import {Collection, IModel} from 'mobx-collection-store'; 4 | 5 | import ICache from './interfaces/ICache'; 6 | import IDictionary from './interfaces/IDictionary'; 7 | import IHeaders from './interfaces/IHeaders'; 8 | import IRequestOptions from './interfaces/IRequestOptions'; 9 | import * as JsonApi from './interfaces/JsonApi'; 10 | 11 | import {NetworkStore} from './NetworkStore'; 12 | import {fetch, prefixUrl, read} from './NetworkUtils'; 13 | import {Record} from './Record'; 14 | import {Response} from './Response'; 15 | import {flattenRecord, mapItems} from './utils'; 16 | 17 | interface IQueryParams { 18 | url: string; 19 | data?: object; 20 | headers: IHeaders; 21 | } 22 | 23 | export class Store extends NetworkStore { 24 | 25 | /** 26 | * List of Models that will be used in the collection 27 | * 28 | * @static 29 | * 30 | * @memberOf Store 31 | */ 32 | public static types = [Record]; 33 | 34 | /** 35 | * Should the cache be used for API calls when possible 36 | * 37 | * @static 38 | * 39 | * @memberof Store 40 | */ 41 | public static cache = true; 42 | 43 | public static: typeof Store; 44 | 45 | /** 46 | * Cache async actions (can be overriden with force=true) 47 | * 48 | * @private 49 | * 50 | * @memberOf Store 51 | */ 52 | private __cache: ICache = { 53 | fetch: {}, 54 | fetchAll: {}, 55 | }; 56 | 57 | /** 58 | * Import the JSON API data into the store 59 | * 60 | * @param {IJsonApiResponse} body - JSON API response 61 | * @returns {(IModel|Array)} - Models parsed from body.data 62 | * 63 | * @memberOf Store 64 | */ 65 | @action public sync(body: JsonApi.IResponse): IModel|Array { 66 | const data: IModel|Array = this.__iterateEntries(body, this.__addRecord.bind(this)); 67 | this.__iterateEntries(body, this.__updateRelationships.bind(this)); 68 | return data; 69 | } 70 | 71 | /** 72 | * Fetch the records with the given type and id 73 | * 74 | * @param {string} type Record type 75 | * @param {number|string} type Record id 76 | * @param {boolean} [force] Force fetch (currently not used) 77 | * @param {IRequestOptions} [options] Server options 78 | * @returns {Promise} Resolves with the Response object or rejects with an error 79 | * 80 | * @memberOf Store 81 | */ 82 | public fetch(type: string, id: number|string, force?: boolean, options?: IRequestOptions): Promise { 83 | const query: IQueryParams = this.__prepareQuery(type, id, null, options); 84 | 85 | if (!this.static.cache) { 86 | return this.__doFetch(query, options); 87 | } 88 | 89 | this.__cache.fetch[type] = this.__cache.fetch[type] || {}; 90 | 91 | // TODO: Should we fake the cache if the record already exists? 92 | if (force || !(query.url in this.__cache.fetch[type])) { 93 | this.__cache.fetch[type][query.url] = this.__doFetch(query, options) 94 | .catch((e) => { 95 | // Don't cache if there was an error 96 | delete this.__cache.fetch[type][query.url]; 97 | throw e; 98 | }); 99 | } 100 | 101 | return this.__cache.fetch[type][query.url]; 102 | } 103 | 104 | /** 105 | * Fetch the first page of records of the given type 106 | * 107 | * @param {string} type Record type 108 | * @param {boolean} [force] Force fetch (currently not used) 109 | * @param {IRequestOptions} [options] Server options 110 | * @returns {Promise} Resolves with the Response object or rejects with an error 111 | * 112 | * @memberOf Store 113 | */ 114 | public fetchAll(type: string, force?: boolean, options?: IRequestOptions): Promise { 115 | const query: IQueryParams = this.__prepareQuery(type, null, null, options); 116 | 117 | if (!this.static.cache) { 118 | return this.__doFetch(query, options); 119 | } 120 | 121 | this.__cache.fetchAll[type] = this.__cache.fetchAll[type] || {}; 122 | if (force || !(query.url in this.__cache.fetchAll[type])) { 123 | this.__cache.fetchAll[type][query.url] = this.__doFetch(query, options) 124 | .catch((e) => { 125 | // Don't cache if there was an error 126 | delete this.__cache.fetchAll[type][query.url]; 127 | throw e; 128 | }); 129 | } 130 | 131 | return this.__cache.fetchAll[type][query.url]; 132 | } 133 | 134 | /** 135 | * Destroy a record (API & store) 136 | * 137 | * @param {string} type Record type 138 | * @param {(number|string)} id Record id 139 | * @param {IRequestOptions} [options] Server options 140 | * @returns {Promise} Resolves true or rejects with an error 141 | * 142 | * @memberOf Store 143 | */ 144 | public destroy(type: string, id: number|string, options?: IRequestOptions): Promise { 145 | const model: Record = this.find(type, id) as Record; 146 | if (model) { 147 | return model.remove(options); 148 | } 149 | return Promise.resolve(true); 150 | } 151 | 152 | public reset() { 153 | super.reset(); 154 | this.__cache.fetch = {}; 155 | this.__cache.fetchAll = {}; 156 | } 157 | 158 | public request(url: string, method: string = 'GET', data?: object, options?: IRequestOptions): Promise { 159 | return fetch({url: prefixUrl(url), options, data, method, store: this}); 160 | } 161 | 162 | public removeAll(type: string): Array { 163 | const models = super.removeAll(type); 164 | this.__cache.fetch[type] = {}; 165 | this.__cache.fetchAll[type] = {}; 166 | return models; 167 | } 168 | 169 | /** 170 | * Make the request and handle the errors 171 | * 172 | * @param {IQueryParams} query Request query info 173 | * @param {IRequestOptions} [options] Server options 174 | * @returns {Promise} Resolves with the Response object or rejects with an error 175 | * 176 | * @memberof Store 177 | */ 178 | private __doFetch(query: IQueryParams, options?: IRequestOptions): Promise { 179 | return read(this, query.url, query.headers, options).then(this.__handleErrors); 180 | } 181 | 182 | /** 183 | * Function used to handle response errors 184 | * 185 | * @private 186 | * @param {Response} response API response 187 | * @returns API response 188 | * 189 | * @memberOf Store 190 | */ 191 | private __handleErrors(response: Response) { 192 | 193 | /* istanbul ignore if */ 194 | if (response.error) { 195 | throw response.error; 196 | } 197 | 198 | return response; 199 | } 200 | 201 | /** 202 | * Add a new JSON API record to the store 203 | * 204 | * @private 205 | * @param {IJsonApiRecord} obj - Object to be added 206 | * @returns {IModel} 207 | * 208 | * @memberOf Store 209 | */ 210 | private __addRecord(obj: JsonApi.IRecord): Record { 211 | const {type, id} = obj; 212 | let record: Record = this.find(type, id) as Record; 213 | const flattened: IDictionary = flattenRecord(obj); 214 | 215 | if (record) { 216 | record.update(flattened); 217 | } else if (this.static.types.filter((item) => item.type === obj.type).length) { 218 | record = this.add(flattened, obj.type) as Record; 219 | } else { 220 | record = new Record(flattened); 221 | this.add(record); 222 | } 223 | 224 | // In case a record is not a real record 225 | // TODO: Figure out when this happens and try to handle it better 226 | /* istanbul ignore else */ 227 | if (record && typeof record.setPersisted === 'function') { 228 | record.setPersisted(true); 229 | } 230 | 231 | return record; 232 | } 233 | 234 | /** 235 | * Update the relationships between models 236 | * 237 | * @private 238 | * @param {IJsonApiRecord} obj - Object to be updated 239 | * @returns {void} 240 | * 241 | * @memberOf Store 242 | */ 243 | private __updateRelationships(obj: JsonApi.IRecord): void { 244 | const record: IModel = this.find(obj.type, obj.id); 245 | const refs: Array = obj.relationships ? Object.keys(obj.relationships) : []; 246 | refs.forEach((ref: string) => { 247 | if (!('data' in obj.relationships[ref])) { 248 | return; 249 | } 250 | const items = obj.relationships[ref].data; 251 | if (items instanceof Array && items.length < 1) { 252 | if (!(ref in record) || ref in record['__data']) { // Hack to check if it's not a back ref 253 | record.assignRef(ref, []); 254 | } 255 | } else if (record) { 256 | if (items) { 257 | const models: IModel|Array = mapItems(items, ({id, type}) => this.find(type, id) || id); 258 | const itemType: string = items instanceof Array ? items[0].type : items.type; 259 | record.assignRef(ref, models, itemType); 260 | } else { 261 | record.assignRef(ref, null); 262 | } 263 | } 264 | }); 265 | } 266 | 267 | /** 268 | * Iterate trough JSON API response models 269 | * 270 | * @private 271 | * @param {IJsonApiResponse} body - JSON API response 272 | * @param {Function} fn - Function to call for every instance 273 | * @returns 274 | * 275 | * @memberOf Store 276 | */ 277 | private __iterateEntries(body: JsonApi.IResponse, fn: Function) { 278 | mapItems((body && body.included) || [], fn); 279 | return mapItems((body && body.data) || [], fn); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/enums/ParamArrayType.ts: -------------------------------------------------------------------------------- 1 | enum ParamArrayType { 2 | MULTIPLE_PARAMS, // filter[a]=1&filter[a]=2 3 | COMMA_SEPARATED, // filter[a]=1,2 4 | PARAM_ARRAY, // filter[a][]=1&filter[a][]=2 5 | OBJECT_PATH, // filter[a.0]=1&filter[a.1]=2 6 | } 7 | 8 | export default ParamArrayType; 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as JsonApi from './interfaces/JsonApi'; 2 | 3 | export {JsonApi}; 4 | export {Store} from './Store'; 5 | export {Record} from './Record'; 6 | export {Response} from './Response'; 7 | export * from './NetworkUtils'; 8 | 9 | export {default as ICache} from './interfaces/ICache'; 10 | export {default as IDictionary} from './interfaces/IDictionary'; 11 | export {default as IFilters} from './interfaces/IFilters'; 12 | export {default as IHeaders} from './interfaces/IHeaders'; 13 | export {default as IRawResponse} from './interfaces/IRawResponse'; 14 | export {default as IRequestOptions} from './interfaces/IRequestOptions'; 15 | export {default as IResponseHeaders} from './interfaces/IResponseHeaders'; 16 | 17 | export {default as ParamArrayType} from './enums/ParamArrayType'; 18 | 19 | export {ICollection, IModel, IModelConstructor, IReferences} from 'mobx-collection-store'; 20 | -------------------------------------------------------------------------------- /src/interfaces/ICache.ts: -------------------------------------------------------------------------------- 1 | import IDictionary from './IDictionary'; 2 | 3 | import {Response} from '../Response'; 4 | 5 | interface ICache { 6 | fetchAll: IDictionary>>; 7 | fetch: IDictionary>>; 8 | } 9 | 10 | export default ICache; 11 | -------------------------------------------------------------------------------- /src/interfaces/IDictionary.ts: -------------------------------------------------------------------------------- 1 | interface IDictionary { 2 | [key: string]: T; 3 | } 4 | 5 | export default IDictionary; 6 | -------------------------------------------------------------------------------- /src/interfaces/IFilters.ts: -------------------------------------------------------------------------------- 1 | interface IFilters { 2 | [key: string]: number|string|Array|Array|IFilters; 3 | } 4 | 5 | export default IFilters; 6 | -------------------------------------------------------------------------------- /src/interfaces/IHeaders.ts: -------------------------------------------------------------------------------- 1 | import IDictionary from './IDictionary'; 2 | 3 | type IHeaders = IDictionary; 4 | export default IHeaders; 5 | -------------------------------------------------------------------------------- /src/interfaces/IRawResponse.ts: -------------------------------------------------------------------------------- 1 | import {Store} from '../Store'; 2 | import IHeaders from './IHeaders'; 3 | import IResponseHeaders from './IResponseHeaders'; 4 | import * as JsonApi from './JsonApi'; 5 | 6 | interface IRawResponse { 7 | data?: JsonApi.IResponse; 8 | error?: Error; 9 | headers?: IResponseHeaders; 10 | requestHeaders?: IHeaders; 11 | status?: number; 12 | jsonapi?: JsonApi.IJsonApiObject; 13 | store?: Store; 14 | } 15 | 16 | export default IRawResponse; 17 | -------------------------------------------------------------------------------- /src/interfaces/IRequestOptions.ts: -------------------------------------------------------------------------------- 1 | import IDictionary from './IDictionary'; 2 | import IFilters from './IFilters'; 3 | import IHeaders from './IHeaders'; 4 | 5 | interface IRequestOptions { 6 | headers?: IHeaders; 7 | include?: string|Array; 8 | filter?: IFilters; 9 | sort?: string|Array; 10 | fields?: IDictionary>; 11 | params?: Array<{key: string, value: string}|string>; 12 | } 13 | 14 | export default IRequestOptions; 15 | -------------------------------------------------------------------------------- /src/interfaces/IResponseHeaders.ts: -------------------------------------------------------------------------------- 1 | interface IResponseHeaders { 2 | get(name: string): string; 3 | } 4 | 5 | export default IResponseHeaders; 6 | -------------------------------------------------------------------------------- /src/interfaces/JsonApi.ts: -------------------------------------------------------------------------------- 1 | import IDictionary from './IDictionary'; 2 | 3 | interface IIdentifier { 4 | id?: number|string; 5 | type: string; 6 | } 7 | 8 | interface IJsonApiObject { 9 | version?: string; 10 | meta?: IDictionary; 11 | } 12 | 13 | type ILink = string | {href: string, meta: IDictionary}; 14 | 15 | interface IError { 16 | id?: string|number; 17 | links?: { 18 | about: ILink, 19 | }; 20 | status?: number; 21 | code?: string; 22 | title?: string; 23 | detail?: string; 24 | source?: { 25 | pointer?: string, 26 | parameter?: string, 27 | }; 28 | meta?: IDictionary; 29 | } 30 | 31 | interface IRelationship { 32 | data?: IIdentifier|Array; 33 | links?: IDictionary; 34 | meta?: IDictionary; 35 | } 36 | 37 | interface IRecord extends IIdentifier { 38 | attributes: IDictionary; 39 | 40 | relationships?: IDictionary; 41 | links?: IDictionary; 42 | meta?: IDictionary; 43 | } 44 | 45 | interface IResponse { 46 | data?: IRecord|Array; 47 | errors?: Array; 48 | 49 | included?: Array; 50 | 51 | meta?: IDictionary; 52 | links?: IDictionary; 53 | jsonapi?: IJsonApiObject; 54 | } 55 | 56 | type IRequest = IResponse; // Not sure if this is correct, but it's ok for now 57 | 58 | export { 59 | IIdentifier, 60 | IJsonApiObject, 61 | ILink, 62 | IError, 63 | IRelationship, 64 | IRecord, 65 | IResponse, 66 | IRequest, 67 | }; 68 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import IDictionary from './interfaces/IDictionary'; 2 | import * as JsonApi from './interfaces/JsonApi'; 3 | 4 | /** 5 | * Iterate trough object keys 6 | * 7 | * @param {object} obj - Object that needs to be iterated 8 | * @param {Function} fn - Function that should be called for every iteration 9 | */ 10 | export function objectForEach(obj: object, fn: Function): void { 11 | for (const key in obj) { 12 | 13 | /* istanbul ignore else */ 14 | if (obj.hasOwnProperty(key)) { 15 | fn(key); 16 | } 17 | } 18 | } 19 | 20 | /** 21 | * Iterate trough one item or array of items and call the defined function 22 | * 23 | * @export 24 | * @template T 25 | * @param {(object|Array)} data - Data which needs to be iterated 26 | * @param {Function} fn - Function that needs to be callse 27 | * @returns {(T|Array)} - The result of iteration 28 | */ 29 | export function mapItems(data: object|Array, fn: Function): T|Array { 30 | return data instanceof Array ? data.map((item) => fn(item)) : fn(data); 31 | } 32 | 33 | /** 34 | * Flatten the JSON API record so it can be inserted into the model 35 | * 36 | * @export 37 | * @param {IJsonApiRecord} record - original JSON API record 38 | * @returns {IDictionary} - Flattened object 39 | */ 40 | export function flattenRecord(record: JsonApi.IRecord): IDictionary { 41 | const data: IDictionary = { 42 | __internal: { 43 | id: record.id, 44 | type: record.type, 45 | }, 46 | }; 47 | 48 | objectForEach(record.attributes, (key) => { 49 | data[key] = record.attributes[key]; 50 | }); 51 | 52 | objectForEach(record.relationships, (key) => { 53 | const rel: JsonApi.IRelationship = record.relationships[key]; 54 | 55 | if (rel.meta) { 56 | data[`${key}Meta`] = rel.meta; 57 | } 58 | 59 | if (rel.links) { 60 | data.__internal.relationships = data.__internal.relationships || {}; 61 | data.__internal.relationships[key] = rel.links; 62 | } 63 | }); 64 | 65 | objectForEach(record.links, (key) => { 66 | 67 | /* istanbul ignore else */ 68 | if (record.links[key]) { 69 | data.__internal.links = data.__internal.links || {}; 70 | data.__internal.links[key] = record.links[key]; 71 | } 72 | }); 73 | 74 | objectForEach(record.meta, (key) => { 75 | 76 | /* istanbul ignore else */ 77 | if (record.meta[key] !== undefined) { 78 | data.__internal.meta = data.__internal.meta || {}; 79 | data.__internal.meta[key] = record.meta[key]; 80 | } 81 | }); 82 | 83 | return data; 84 | } 85 | 86 | export const isBrowser: boolean = (typeof window !== 'undefined'); 87 | 88 | /** 89 | * Assign objects to the target object 90 | * Not a complete implementation (Object.assign) 91 | * Based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign polyfill 92 | * 93 | * @private 94 | * @param {object} target - Target object 95 | * @param {Array} args - Objects to be assigned 96 | * @returns 97 | */ 98 | export function assign(target: object, ...args: Array) { 99 | args.forEach((nextSource: object) => { 100 | if (nextSource != null) { 101 | for (const nextKey in nextSource) { 102 | 103 | /* istanbul ignore else */ 104 | if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { 105 | target[nextKey] = nextSource[nextKey]; 106 | } 107 | } 108 | } 109 | }); 110 | return target; 111 | } 112 | 113 | /** 114 | * Returns the value if it's not a function. If it's a function 115 | * it calls it. 116 | * 117 | * @export 118 | * @template T 119 | * @param {(T|(() => T))} target can be anything or function 120 | * @returns {T} value 121 | */ 122 | export function getValue(target: T|(() => T)): T { 123 | if (typeof target === 'function') { 124 | // @ts-ignore 125 | return target(); 126 | } 127 | 128 | return target; 129 | } 130 | 131 | /** 132 | * Get all object keys 133 | * 134 | * @export 135 | * @param {object} obj Object to process 136 | * @returns {Array} List of object keys 137 | */ 138 | export function keys(obj: object): Array { 139 | const keyList = []; 140 | for (const key in obj) { 141 | if (obj.hasOwnProperty(key)) { 142 | keyList.push(key); 143 | } 144 | } 145 | return keyList; 146 | } 147 | -------------------------------------------------------------------------------- /test/issues.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import * as fetch from 'isomorphic-fetch'; 3 | import {computed} from 'mobx'; 4 | 5 | // tslint:disable:no-string-literal 6 | // tslint:disable:max-classes-per-file 7 | 8 | import {config, Record, Store} from '../src'; 9 | 10 | import mockApi from './utils/api'; 11 | import {Event, Image, Organiser, Photo, TestStore, User} from './utils/setup'; 12 | 13 | describe('Reported issues', () => { 14 | beforeEach(() => { 15 | config.fetchReference = fetch; 16 | config.baseUrl = 'http://example.com/'; 17 | }); 18 | 19 | describe('Issue #29', () => { 20 | it('should return both units', async () => { 21 | class UnitRecord extends Record { 22 | public static type = 'units'; 23 | public static refs = { organization: 'organizations' }; 24 | 25 | public organization?: OrganizationRecord; 26 | } 27 | 28 | class OrganizationRecord extends Record { 29 | public static type = 'organizations'; 30 | public static refs = { 31 | units: { 32 | model: 'units', 33 | property: 'organization', 34 | }, 35 | }; 36 | 37 | public name: string; 38 | public units: Array; 39 | } 40 | 41 | class ApiStore extends Store { 42 | public static types = [OrganizationRecord, UnitRecord]; 43 | 44 | public organizations: Array; 45 | public units: Array; 46 | } 47 | 48 | const store = new ApiStore(); 49 | 50 | mockApi({ 51 | name: 'issue-29', 52 | query: {include: 'units'}, 53 | url: 'organizations', 54 | }); 55 | 56 | const response = await store.fetchAll('organizations', undefined, {include: 'units'}); 57 | 58 | const org = response.data[0] as OrganizationRecord; 59 | 60 | expect(org.getRecordId()).to.equal('ORG-A'); 61 | expect(org.units).to.be.an('array'); 62 | }); 63 | }); 64 | 65 | describe('fetch issues', () => { 66 | beforeEach(() => { 67 | this.defaultFetch = config.fetchReference; 68 | }); 69 | 70 | afterEach(() => { 71 | config.fetchReference = this.defaultFetch; 72 | }); 73 | 74 | it('should not send a body on GET and HEAD', async () => { 75 | config.fetchReference = async (url, options) => { 76 | expect(options.body).to.be.an('undefined'); 77 | 78 | // Mock response 79 | return { 80 | status: 204, 81 | async json() { 82 | return {}; 83 | }, 84 | }; 85 | }; 86 | 87 | const store = new Store(); 88 | const events = await store.fetchAll('event'); 89 | }); 90 | }); 91 | 92 | describe('save creates a new model', () => { 93 | it('should update the existing generic model', async () => { 94 | const store = new Store(); 95 | const record = new Record({ 96 | password: 'hunter2', 97 | username: 'foobar', 98 | }, 'sessions'); 99 | store.add(record); 100 | 101 | mockApi({ 102 | method: 'POST', 103 | name: 'session-1', 104 | url: 'sessions', 105 | }); 106 | 107 | const updated = await record.save(); 108 | expect(updated).to.equal(record); 109 | expect(store.length).to.equal(1); 110 | }); 111 | 112 | it('should update the existing custom model', async () => { 113 | class Session extends Record {} 114 | Session.type = 'sessions'; 115 | Session.endpoint = 'sessions'; 116 | 117 | class AppStore extends Store {} 118 | AppStore.types = [Session]; 119 | const store = new AppStore(); 120 | 121 | mockApi({ 122 | method: 'POST', 123 | name: 'session-1', 124 | url: 'sessions', 125 | }); 126 | 127 | const login = new Session({email: 'test@example.com', password: 'hunter2'}); 128 | store.add(login); 129 | const session = await login.save(); 130 | 131 | expect(session).to.equal(login); 132 | expect(store.length).to.equal(1); 133 | expect(store.find('sessions', 12345)).to.equal(session); 134 | }); 135 | }); 136 | 137 | describe('wrong toJsonApi references when null', () => { 138 | it('should work', () => { 139 | class UnitRecord extends Record { 140 | public static type = 'units'; 141 | public static refs = { organization: 'organizations' }; 142 | 143 | public organization?: OrganizationRecord; 144 | public organizationId?: number|string; 145 | } 146 | 147 | class OrganizationRecord extends Record { 148 | public static type = 'organizations'; 149 | public static refs = { 150 | units: { 151 | model: 'units', 152 | property: 'organization', 153 | }, 154 | }; 155 | 156 | public name: string; 157 | public units: Array; 158 | } 159 | 160 | class ApiStore extends Store { 161 | public static types = [OrganizationRecord, UnitRecord]; 162 | 163 | public organizations: Array; 164 | public units: Array; 165 | } 166 | 167 | const store = new ApiStore(); 168 | const unit = new UnitRecord(); 169 | 170 | expect(unit.toJsonApi().relationships.organization.data).to.equal(null); 171 | 172 | store.add(unit); 173 | 174 | expect(unit.toJsonApi().relationships.organization.data).to.equal(null); 175 | 176 | unit.organization = new OrganizationRecord({name: 'Foo'}); 177 | expect(unit.toJsonApi().relationships.organization.data['id']).to.equal(unit.organizationId); 178 | expect(unit.toJsonApi().relationships.organization.data['type']).to.equal('organizations'); 179 | }); 180 | }); 181 | 182 | describe('Issue #84 - Server response with null reference', () => { 183 | it('should remove the reference if null', async () => { 184 | class EventRecord extends Record { 185 | public static type = 'event'; 186 | public static refs = { image: 'image' }; 187 | 188 | public image?: ImageRecord|Array; 189 | } 190 | 191 | class ImageRecord extends Record { 192 | public static type = 'image'; 193 | 194 | public name: string; 195 | public event: Array; 196 | } 197 | 198 | class ApiStore extends Store { 199 | public static types = [ImageRecord, EventRecord]; 200 | 201 | public image: Array; 202 | public event: Array; 203 | } 204 | 205 | const store = new ApiStore(); 206 | 207 | mockApi({ 208 | name: 'issue-84a', 209 | url: 'event/1', 210 | }); 211 | 212 | const response = await store.fetch('event', 1); 213 | 214 | const event = response.data as EventRecord; 215 | 216 | expect(event.image).to.equal(store.image[0]); 217 | 218 | mockApi({ 219 | name: 'issue-84b', 220 | url: 'event/1', 221 | }); 222 | await store.fetch('event', 1, true); 223 | expect(event.image).to.equal(null); 224 | 225 | mockApi({ 226 | name: 'issue-84a', 227 | url: 'event/1', 228 | }); 229 | await store.fetch('event', 1, true); 230 | expect(event.image).to.equal(store.image[0]); 231 | 232 | mockApi({ 233 | name: 'issue-84d', 234 | url: 'event/1', 235 | }); 236 | await store.fetch('event', 1, true); 237 | expect(event.image['length']).to.equal(1); 238 | expect(event.image[0]).to.equal(store.image[0]); 239 | 240 | mockApi({ 241 | name: 'issue-84e', 242 | url: 'event/1', 243 | }); 244 | await store.fetch('event', 1, true); 245 | expect(event.image['length']).to.equal(0); 246 | }); 247 | 248 | it('should update the reference if not null', async () => { 249 | class EventRecord extends Record { 250 | public static type = 'event'; 251 | public static refs = { image: 'image' }; 252 | 253 | public image?: ImageRecord; 254 | } 255 | 256 | class ImageRecord extends Record { 257 | public static type = 'image'; 258 | 259 | public name: string; 260 | public event: Array; 261 | } 262 | 263 | class ApiStore extends Store { 264 | public static types = [ImageRecord, EventRecord]; 265 | 266 | public image: Array; 267 | public event: Array; 268 | } 269 | 270 | const store = new ApiStore(); 271 | 272 | mockApi({ 273 | name: 'issue-84a', 274 | url: 'event/1', 275 | }); 276 | 277 | const response = await store.fetch('event', 1); 278 | 279 | const event = response.data as EventRecord; 280 | 281 | expect(event.image).to.equal(store.image[0]); 282 | 283 | mockApi({ 284 | name: 'issue-84c', 285 | url: 'event/1', 286 | }); 287 | await store.fetch('event', 1, true); 288 | expect(event.image).to.equal(store.image[1]); 289 | 290 | mockApi({ 291 | name: 'issue-84a', 292 | url: 'event/1', 293 | }); 294 | await store.fetch('event', 1, true); 295 | expect(event.image).to.equal(store.image[0]); 296 | }); 297 | }); 298 | }); 299 | -------------------------------------------------------------------------------- /test/main.ts: -------------------------------------------------------------------------------- 1 | import './general'; 2 | 3 | import './network/basics'; 4 | import './network/caching'; 5 | import './network/error-handling'; 6 | import './network/headers'; 7 | import './network/params'; 8 | import './network/updates'; 9 | 10 | import './issues'; 11 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --full-trace 3 | --check-leaks 4 | --recursive 5 | --bail 6 | src/**/*.ts test/main.ts -------------------------------------------------------------------------------- /test/mock/error.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [{ 3 | "title": "Unknown error", 4 | "status": "123" 5 | }] 6 | } -------------------------------------------------------------------------------- /test/mock/event-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": 12345, 4 | "type": "event", 5 | "attributes": { 6 | "title": "Test 1", 7 | "date": "2017-03-19" 8 | }, 9 | "links": { 10 | "image": "http://example.com/images/1" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /test/mock/event-1b.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": 1, 4 | "type": "event", 5 | "attributes": { 6 | "title": "Test 1", 7 | "date": "2017-03-19" 8 | }, 9 | "links": { 10 | "self": "http://example.com/event/1234" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /test/mock/event-1c.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": "110ec58a-a0f2-4ac4-8393-c866d813b8d1", 4 | "type": "event", 5 | "attributes": { 6 | "title": "Test 1", 7 | "date": "2017-03-19" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /test/mock/event-1d.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": 12345, 4 | "type": "event", 5 | "attributes": { 6 | "title": "Test 1", 7 | "date": "2017-03-19" 8 | }, 9 | "meta": { 10 | "createdAt": "2017-03-19T16:00:00.000Z" 11 | }, 12 | "relationships": { 13 | "image": { 14 | "data": [{ 15 | "type": "image", 16 | "id": "1" 17 | }, { 18 | "type": "image", 19 | "id": "2" 20 | }], 21 | "links": { 22 | "self": "http://example.com/images/1" 23 | }, 24 | "meta": { 25 | "foo": "bar" 26 | } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/mock/events-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [{ 3 | "id": 1, 4 | "type": "event", 5 | "attributes": { 6 | "title": "Test 1", 7 | "date": "2017-03-19" 8 | }, 9 | "meta": { 10 | "createdAt": "2017-03-19T16:00:00.000Z" 11 | }, 12 | "relationships": { 13 | "image": { 14 | "data": { 15 | "type": "image", 16 | "id": "1" 17 | }, 18 | "links": { 19 | "self": "http://example.com/images/1" 20 | }, 21 | "meta": { 22 | "foo": "bar" 23 | } 24 | } 25 | } 26 | }, { 27 | "id": 2, 28 | "type": "event", 29 | "attributes": { 30 | "title": "Test 2", 31 | "date": "2017-03-20" 32 | } 33 | }, { 34 | "id": 3, 35 | "type": "event", 36 | "attributes": { 37 | "title": "Test 3", 38 | "date": "2017-03-21" 39 | } 40 | }, { 41 | "id": 4, 42 | "type": "event", 43 | "attributes": { 44 | "title": "Test 4", 45 | "date": "2017-03-22" 46 | } 47 | }], 48 | "links": { 49 | "self": "http://example.com/event", 50 | "next": { 51 | "href": "http://example.com/event?page=2", 52 | "meta": { 53 | "foo": "bar" 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /test/mock/events-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [{ 3 | "id": 5, 4 | "type": "event", 5 | "attributes": { 6 | "title": "Test 5", 7 | "date": "2017-03-23" 8 | } 9 | }, { 10 | "id": 6, 11 | "type": "event", 12 | "attributes": { 13 | "title": "Test 6", 14 | "date": "2017-03-24" 15 | } 16 | }], 17 | "links": { 18 | "prev": "http://example.com/event" 19 | } 20 | } -------------------------------------------------------------------------------- /test/mock/image-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": 1, 4 | "type": "image", 5 | "attributes": { 6 | "url": "http://example.com/1.jpg" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /test/mock/invalid.json: -------------------------------------------------------------------------------- 1 |

Not found

-------------------------------------------------------------------------------- /test/mock/issue-29.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi":{ 3 | "version":"1.0" 4 | }, 5 | "meta":{ 6 | "page":{ 7 | "offset":0, 8 | "limit":50, 9 | "total":1 10 | } 11 | }, 12 | "links":{ 13 | "self":"http://localhost:5000/organizations?include=units" 14 | }, 15 | "data":[ 16 | { 17 | "type":"organizations", 18 | "id":"ORG-A", 19 | "attributes":{ 20 | "name":"Organization A" 21 | }, 22 | "links":{ 23 | "self":"http://localhost:5000/organizations/ORG-A" 24 | }, 25 | "relationships":{ 26 | "units":{ 27 | "meta":{ 28 | "relation":"foreign", 29 | "belongsTo":"units", 30 | "as":"organization", 31 | "many":true, 32 | "readOnly":true 33 | }, 34 | "links":{ 35 | "self":"http://localhost:5000/units/relationships/?organization=ORG-A", 36 | "related":"http://localhost:5000/units/?filter[organization]=ORG-A" 37 | }, 38 | "data": [] 39 | } 40 | } 41 | } 42 | ], 43 | "included":[ 44 | { 45 | "type":"units", 46 | "id":"ORG-A-01", 47 | "attributes":{ 48 | "name":"Unit 01" 49 | }, 50 | "links":{ 51 | "self":"http://localhost:5000/units/ORG-A-01" 52 | }, 53 | "relationships":{ 54 | "organization":{ 55 | "meta":{ 56 | "relation":"primary", 57 | "readOnly":false 58 | }, 59 | "links":{ 60 | "self":"http://localhost:5000/units/ORG-A-01/relationships/organization", 61 | "related":"http://localhost:5000/units/ORG-A-01/organization" 62 | }, 63 | "data":{ 64 | "type":"organizations", 65 | "id":"ORG-A" 66 | } 67 | }, 68 | "orders":{ 69 | "meta":{ 70 | "relation":"foreign", 71 | "belongsTo":"orders", 72 | "as":"unit", 73 | "many":true, 74 | "readOnly":true 75 | }, 76 | "links":{ 77 | "self":"http://localhost:5000/orders/relationships/?unit=ORG-A-01", 78 | "related":"http://localhost:5000/orders/?filter[unit]=ORG-A-01" 79 | } 80 | } 81 | } 82 | }, 83 | { 84 | "type":"units", 85 | "id":"ORG-A-02", 86 | "attributes":{ 87 | "name":"Unit 02" 88 | }, 89 | "links":{ 90 | "self":"http://localhost:5000/units/ORG-A-02" 91 | }, 92 | "relationships":{ 93 | "organization":{ 94 | "meta":{ 95 | "relation":"primary", 96 | "readOnly":false 97 | }, 98 | "links":{ 99 | "self":"http://localhost:5000/units/ORG-A-02/relationships/organization", 100 | "related":"http://localhost:5000/units/ORG-A-02/organization" 101 | }, 102 | "data":{ 103 | "type":"organizations", 104 | "id":"ORG-A" 105 | } 106 | }, 107 | "orders":{ 108 | "meta":{ 109 | "relation":"foreign", 110 | "belongsTo":"orders", 111 | "as":"unit", 112 | "many":true, 113 | "readOnly":true 114 | }, 115 | "links":{ 116 | "self":"http://localhost:5000/orders/relationships/?unit=ORG-A-02", 117 | "related":"http://localhost:5000/orders/?filter[unit]=ORG-A-02" 118 | } 119 | } 120 | } 121 | } 122 | ] 123 | } -------------------------------------------------------------------------------- /test/mock/issue-84a.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": 12345, 4 | "type": "event", 5 | "attributes": { 6 | "title": "Test 1", 7 | "date": "2017-03-19" 8 | }, 9 | "meta": { 10 | "createdAt": "2017-03-19T16:00:00.000Z" 11 | }, 12 | "relationships": { 13 | "image": { 14 | "data": { 15 | "type": "image", 16 | "id": "1" 17 | } 18 | } 19 | } 20 | }, 21 | "included": [{ 22 | "id": 1, 23 | "type": "image", 24 | "attributes": { 25 | "url": "http://example.com/1.jpg" 26 | } 27 | }] 28 | } 29 | -------------------------------------------------------------------------------- /test/mock/issue-84b.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": 12345, 4 | "type": "event", 5 | "attributes": { 6 | "title": "Test 1", 7 | "date": "2017-03-19" 8 | }, 9 | "meta": { 10 | "createdAt": "2017-03-19T16:00:00.000Z" 11 | }, 12 | "relationships": { 13 | "image": { 14 | "data": null 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/mock/issue-84c.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": 12345, 4 | "type": "event", 5 | "attributes": { 6 | "title": "Test 1", 7 | "date": "2017-03-19" 8 | }, 9 | "meta": { 10 | "createdAt": "2017-03-19T16:00:00.000Z" 11 | }, 12 | "relationships": { 13 | "image": { 14 | "data": { 15 | "type": "image", 16 | "id": "2" 17 | } 18 | } 19 | } 20 | }, 21 | "included": [{ 22 | "id": 2, 23 | "type": "image", 24 | "attributes": { 25 | "url": "http://example.com/2.jpg" 26 | } 27 | }] 28 | } 29 | -------------------------------------------------------------------------------- /test/mock/issue-84d.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": 12345, 4 | "type": "event", 5 | "attributes": { 6 | "title": "Test 1", 7 | "date": "2017-03-19" 8 | }, 9 | "meta": { 10 | "createdAt": "2017-03-19T16:00:00.000Z" 11 | }, 12 | "relationships": { 13 | "image": { 14 | "data": [{ 15 | "type": "image", 16 | "id": "1" 17 | }] 18 | } 19 | } 20 | }, 21 | "included": [{ 22 | "id": 1, 23 | "type": "image", 24 | "attributes": { 25 | "url": "http://example.com/1.jpg" 26 | } 27 | }] 28 | } 29 | -------------------------------------------------------------------------------- /test/mock/issue-84e.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": 12345, 4 | "type": "event", 5 | "attributes": { 6 | "title": "Test 1", 7 | "date": "2017-03-19" 8 | }, 9 | "meta": { 10 | "createdAt": "2017-03-19T16:00:00.000Z" 11 | }, 12 | "relationships": { 13 | "image": { 14 | "data": [] 15 | } 16 | } 17 | }, 18 | "included": [{ 19 | "id": 1, 20 | "type": "image", 21 | "attributes": { 22 | "url": "http://example.com/1.jpg" 23 | } 24 | }] 25 | } 26 | -------------------------------------------------------------------------------- /test/mock/issue-falsy-meta-value.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": 1, 4 | "type": "event", 5 | "attributes": {}, 6 | "meta": { 7 | "count": 0 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/mock/jsonapi-object.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.0", 4 | "meta": { 5 | "foo": "bar" 6 | } 7 | }, 8 | "data": [{ 9 | "id": 1, 10 | "type": "event", 11 | "attributes": { 12 | "title": "Test 1", 13 | "date": "2017-03-19" 14 | }, 15 | "meta": { 16 | "createdAt": "2017-03-19T16:00:00.000Z" 17 | } 18 | }, { 19 | "id": 2, 20 | "type": "event", 21 | "attributes": { 22 | "title": "Test 2", 23 | "date": "2017-03-20" 24 | } 25 | }, { 26 | "id": 3, 27 | "type": "event", 28 | "attributes": { 29 | "title": "Test 3", 30 | "date": "2017-03-21" 31 | } 32 | }, { 33 | "id": 4, 34 | "type": "event", 35 | "attributes": { 36 | "title": "Test 4", 37 | "date": "2017-03-22" 38 | } 39 | }], 40 | "links": { 41 | "self": "http://example.com/event", 42 | "next": "http://example.com/event?page=2" 43 | } 44 | } -------------------------------------------------------------------------------- /test/mock/queue-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "type": "queue", 4 | "id": "5234", 5 | "attributes": { 6 | "status": "Pending request, waiting other process" 7 | }, 8 | "links": { 9 | "self": "http://example.com/events/queue-jobs/123" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/mock/session-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": 12345, 4 | "type": "sessions", 5 | "attributes": { 6 | "token": "test123" 7 | }, 8 | "links": { 9 | "image": "http://example.com/user/1" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/network/basics.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import * as fetch from 'isomorphic-fetch'; 3 | 4 | // tslint:disable:no-string-literal 5 | 6 | import {config, Record, Store} from '../../src'; 7 | 8 | import mockApi from '../utils/api'; 9 | import {Event, Image, Organiser, Photo, TestStore, User} from '../utils/setup'; 10 | 11 | const baseStoreFetch = config.storeFetch; 12 | const baseTransformRequest = config.transformRequest; 13 | const baseTransformResponse = config.transformResponse; 14 | 15 | describe('Network basics', () => { 16 | beforeEach(() => { 17 | config.fetchReference = fetch; 18 | config.baseUrl = 'http://example.com/'; 19 | config.storeFetch = baseStoreFetch; 20 | config.transformRequest = baseTransformRequest; 21 | config.transformResponse = baseTransformResponse; 22 | }); 23 | 24 | it('should fetch the basic data', async () => { 25 | mockApi({ 26 | name: 'events-1', 27 | url: 'event', 28 | }); 29 | 30 | const store = new Store(); 31 | const events = await store.fetchAll('event'); 32 | 33 | expect(events.data).to.be.an('array'); 34 | expect(events.data['length']).to.equal(4); 35 | expect(events.data instanceof Array && events.data[0]['title']).to.equal('Test 1'); 36 | expect(events.data[0].getMeta().createdAt).to.equal('2017-03-19T16:00:00.000Z'); 37 | expect(events.data[0]['imageId']).to.equal('1'); 38 | expect(events.data[0]['imageMeta']['foo']).to.equal('bar'); 39 | 40 | const data = events.data[0].toJsonApi(); 41 | expect(data.id).to.equal(1); 42 | expect(data.type).to.equal('event'); 43 | expect(data.attributes.title).to.equal('Test 1'); 44 | expect(data.relationships.image.data).to.eql({type: 'image', id: '1'}); 45 | }); 46 | 47 | it('should use the correct types', async () => { 48 | const store = new TestStore(); 49 | 50 | const user = new User({firstName: 'John'}); 51 | const userData = user.toJsonApi(); 52 | expect(userData.type).to.equal('user'); 53 | 54 | const wrongEvent = new Event({ 55 | name: 'Test', 56 | type: 'foo', 57 | }, 'evil-event'); 58 | const wrongEventData = wrongEvent.toJsonApi(); 59 | expect(wrongEventData.type).to.equal('evil-event'); 60 | expect(wrongEventData.attributes.type).to.equal('foo'); 61 | }); 62 | 63 | it('should support storeFetch override', async () => { 64 | mockApi({ 65 | name: 'events-1', 66 | url: 'event', 67 | }); 68 | 69 | let hasCustomStoreFetchBeenCalled = false; 70 | 71 | config.storeFetch = (opts) => { 72 | expect(opts.store).to.equal(store); 73 | hasCustomStoreFetchBeenCalled = true; 74 | return baseStoreFetch(opts); 75 | }; 76 | 77 | const store = new Store(); 78 | const events = await store.fetchAll('event'); 79 | 80 | expect(events.data).to.be.an('array'); 81 | expect(hasCustomStoreFetchBeenCalled).to.equal(true); 82 | }); 83 | 84 | it('should support transformRequest hook', async () => { 85 | mockApi({ 86 | name: 'events-1', 87 | url: 'event/all', 88 | }); 89 | 90 | let hasTransformRequestHookBeenCalled = false; 91 | 92 | config.transformRequest = (opts) => { 93 | expect(opts.store).to.equal(store); 94 | hasTransformRequestHookBeenCalled = true; 95 | return {...opts, url: `${opts.url}/all`}; 96 | }; 97 | 98 | const store = new Store(); 99 | const events = await store.fetchAll('event'); 100 | 101 | expect(events.data).to.be.an('array'); 102 | expect(hasTransformRequestHookBeenCalled).to.equal(true); 103 | }); 104 | 105 | it('should support transformResponse hook', async () => { 106 | mockApi({ 107 | name: 'events-1', 108 | url: 'event', 109 | }); 110 | 111 | let hasTransformResponseHookBeenCalled = false; 112 | 113 | config.transformResponse = (opts) => { 114 | expect(opts.status).to.equal(200); 115 | hasTransformResponseHookBeenCalled = true; 116 | return {...opts, status: 201}; 117 | }; 118 | 119 | const store = new Store(); 120 | const events = await store.fetchAll('event'); 121 | 122 | expect(events.data).to.be.an('array'); 123 | expect(events.status).to.equal(201); 124 | expect(hasTransformResponseHookBeenCalled).to.equal(true); 125 | }); 126 | 127 | it('should save the jsonapi data', async () => { 128 | mockApi({ 129 | name: 'jsonapi-object', 130 | url: 'event', 131 | }); 132 | 133 | const store = new Store(); 134 | const events = await store.fetchAll('event'); 135 | 136 | expect(events.data).to.be.an('array'); 137 | expect(events.data['length']).to.equal(4); 138 | expect(events.jsonapi).to.be.an('object'); 139 | expect(events.jsonapi.version).to.equal('1.0'); 140 | expect(events.jsonapi.meta.foo).to.equal('bar'); 141 | }); 142 | 143 | it('should fetch one item', async () => { 144 | mockApi({ 145 | name: 'event-1b', 146 | url: 'event/1', 147 | }); 148 | 149 | const store = new Store(); 150 | const events = await store.fetch('event', 1); 151 | 152 | const record = events.data as Record; 153 | 154 | expect(record).to.be.an('object'); 155 | expect(record['title']).to.equal('Test 1'); 156 | expect(record.getLinks()).to.be.an('object'); 157 | expect(record.getLinks().self).to.equal('http://example.com/event/1234'); 158 | }); 159 | 160 | it('should support pagination', async () => { 161 | mockApi({ 162 | name: 'events-1', 163 | url: 'event', 164 | }); 165 | 166 | const store = new Store(); 167 | const events = await store.fetchAll('event'); 168 | 169 | expect(events.data).to.be.an('array'); 170 | expect(events.data['length']).to.equal(4); 171 | expect(events.data instanceof Array && events.data[0]['title']).to.equal('Test 1'); 172 | expect(events.links).to.be.an('object'); 173 | expect(events.links['next']['href']).to.equal('http://example.com/event?page=2'); 174 | expect(events.links['next']['meta'].foo).to.equal('bar'); 175 | 176 | mockApi({ 177 | name: 'events-2', 178 | query: { 179 | page: 2, 180 | }, 181 | url: 'event', 182 | }); 183 | 184 | const events2 = await events.next; 185 | 186 | expect(events2.data).to.be.an('array'); 187 | expect(events2.data['length']).to.equal(2); 188 | expect(events2.data instanceof Array && events2.data[0]['title']).to.equal('Test 5'); 189 | 190 | mockApi({ 191 | name: 'events-1', 192 | url: 'event', 193 | }); 194 | 195 | const events1 = await events2.prev; 196 | 197 | expect(events1.data).to.be.an('array'); 198 | expect(events1.data['length']).to.equal(4); 199 | expect(events1.data instanceof Array && events1.data[0]['title']).to.equal('Test 1'); 200 | 201 | const events1b = await events2.prev; 202 | expect(events1).to.not.equal(events); 203 | expect(events1).to.equal(events1b); 204 | }); 205 | 206 | it('should support record links', async () => { 207 | mockApi({ 208 | name: 'event-1', 209 | url: 'event', 210 | }); 211 | 212 | const store = new Store(); 213 | const events = await store.fetchAll('event'); 214 | const event = events.data as Record; 215 | 216 | mockApi({ 217 | name: 'image-1', 218 | url: 'images/1', 219 | }); 220 | 221 | const image = await event.fetchLink('image'); 222 | const imageData = image.data as Image; 223 | expect(imageData.getRecordId()).to.equal(1); 224 | expect(imageData.getRecordType()).to.equal('image'); 225 | expect(imageData['url']).to.equal('http://example.com/1.jpg'); 226 | }); 227 | 228 | it('should recover if no link defined', async () => { 229 | mockApi({ 230 | name: 'event-1', 231 | url: 'event', 232 | }); 233 | 234 | const store = new Store(); 235 | const events = await store.fetchAll('event'); 236 | const event = events.data as Record; 237 | 238 | const foobar = await event.fetchLink('foobar'); 239 | expect(foobar.data).to.be.an('array'); 240 | expect(foobar.data).to.have.length(0); 241 | }); 242 | 243 | it('should support relationship link fetch', async () => { 244 | mockApi({ 245 | name: 'events-1', 246 | url: 'event', 247 | }); 248 | 249 | const store = new Store(); 250 | const events = await store.fetchAll('event'); 251 | const event = events.data[0] as Record; 252 | 253 | mockApi({ 254 | name: 'image-1', 255 | url: 'images/1', 256 | }); 257 | 258 | const image = await event.fetchRelationshipLink('image', 'self'); 259 | const imageData = image.data as Image; 260 | expect(imageData.getRecordId()).to.equal(1); 261 | expect(imageData.getRecordType()).to.equal('image'); 262 | expect(imageData['url']).to.equal('http://example.com/1.jpg'); 263 | 264 | }); 265 | 266 | it('should support endpoint', async () => { 267 | // tslint:disable-next-line:max-classes-per-file 268 | class EventModel extends Record { 269 | public static type = 'event'; 270 | public static endpoint = 'foo/event'; 271 | } 272 | 273 | // tslint:disable-next-line:max-classes-per-file 274 | class Collection extends Store { 275 | public static types = [EventModel]; 276 | } 277 | 278 | const store = new Collection(); 279 | 280 | mockApi({ 281 | name: 'event-1', 282 | url: 'foo/event', 283 | }); 284 | 285 | const response = await store.fetchAll('event'); 286 | const event = response.data as EventModel; 287 | expect(event.getRecordType()).to.equal('event'); 288 | }); 289 | 290 | it('should support functional endpoint', async () => { 291 | // tslint:disable-next-line:max-classes-per-file 292 | class EventModel extends Record { 293 | public static type = 'event'; 294 | public static endpoint = () => 'foo/event'; 295 | } 296 | 297 | // tslint:disable-next-line:max-classes-per-file 298 | class Collection extends Store { 299 | public static types = [EventModel]; 300 | } 301 | 302 | const store = new Collection(); 303 | 304 | mockApi({ 305 | name: 'event-1', 306 | url: 'foo/event', 307 | }); 308 | 309 | const response = await store.fetchAll('event'); 310 | const event = response.data as EventModel; 311 | expect(event.getRecordType()).to.equal('event'); 312 | }); 313 | 314 | it('should prepend config.baseUrl to the request url', async () => { 315 | mockApi({ 316 | name: 'event-1b', 317 | url: 'event/1', 318 | }); 319 | 320 | const store = new Store(); 321 | const events = await store.request('event/1'); 322 | 323 | const record = events.data as Record; 324 | 325 | expect(record['title']).to.equal('Test 1'); 326 | }); 327 | 328 | it('should handle the request methods', async () => { 329 | mockApi({ 330 | method: 'PUT', 331 | name: 'event-1b', 332 | url: 'event/1', 333 | }); 334 | 335 | const store = new Store(); 336 | const events = await store.request('event/1', 'PUT'); 337 | 338 | const record = events.data as Record; 339 | 340 | expect(record['title']).to.equal('Test 1'); 341 | }); 342 | 343 | it('should support meta fields with falsy values', async () => { 344 | mockApi({ 345 | name: 'issue-falsy-meta-value', 346 | url: 'event/1', 347 | }); 348 | 349 | const store = new Store(); 350 | const events = await store.fetch('event', 1); 351 | 352 | const record = events.data as Record; 353 | 354 | expect(record.getMeta()).to.be.an('object'); 355 | expect(record.getMeta()['count']).to.equal(0); 356 | }); 357 | 358 | }); 359 | -------------------------------------------------------------------------------- /test/network/caching.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import * as fetch from 'isomorphic-fetch'; 3 | 4 | // tslint:disable:no-string-literal 5 | 6 | import {config, Record, Store} from '../../src'; 7 | 8 | import mockApi from '../utils/api'; 9 | import {Event, Image, Organiser, Photo, TestStore, User} from '../utils/setup'; 10 | 11 | const baseStoreFetch = config.storeFetch; 12 | 13 | describe('caching', () => { 14 | beforeEach(() => { 15 | config.fetchReference = fetch; 16 | config.baseUrl = 'http://example.com/'; 17 | }); 18 | 19 | describe('fetch caching', () => { 20 | it('should cache fetch requests', async () => { 21 | mockApi({ 22 | name: 'event-1', 23 | url: 'event/12345', 24 | }); 25 | 26 | const store = new Store(); 27 | const events = await store.fetch('event', 12345); 28 | const event = events.data as Event; 29 | 30 | expect(event).to.be.an('object'); 31 | expect(event.getRecordId()).to.equal(12345); 32 | 33 | const events2 = await store.fetch('event', 12345); 34 | 35 | expect(events2).to.equal(events); 36 | }); 37 | 38 | it('should clear fetch cache on removeAll', async () => { 39 | mockApi({ 40 | name: 'event-1', 41 | url: 'event/12345', 42 | }); 43 | 44 | const store = new Store(); 45 | const events = await store.fetch('event', 12345); 46 | const event = events.data as Event; 47 | 48 | expect(event.getRecordId()).to.equal(12345); 49 | 50 | store.removeAll('event'); 51 | 52 | const req2 = mockApi({ 53 | name: 'event-1', 54 | url: 'event/12345', 55 | }); 56 | 57 | const events2 = await store.fetch('event', 12345); 58 | const event2 = events2.data as Event; 59 | expect(event2.getRecordId()).to.equal(12345); 60 | expect(req2.isDone()).to.equal(true); 61 | }); 62 | 63 | it('should ignore fetch cache if force is true', async () => { 64 | mockApi({ 65 | name: 'event-1', 66 | url: 'event/12345', 67 | }); 68 | 69 | const store = new Store(); 70 | const events = await store.fetch('event', 12345); 71 | const event = events.data as Event; 72 | 73 | expect(event).to.be.an('object'); 74 | expect(event.getRecordId()).to.equal(12345); 75 | 76 | mockApi({ 77 | name: 'event-1', 78 | url: 'event/12345', 79 | }); 80 | 81 | const events2 = await store.fetch('event', 12345, true); 82 | const event2 = events2.data as Event; 83 | 84 | expect(events2).to.not.equal(events); 85 | expect(event2).to.be.an('object'); 86 | expect(event2.getRecordId()).to.equal(12345); 87 | }); 88 | 89 | it('should ignore fetch cache if static cache is false', async () => { 90 | mockApi({ 91 | name: 'event-1', 92 | url: 'event/12345', 93 | }); 94 | 95 | // tslint:disable-next-line:max-classes-per-file 96 | class TestStore2 extends Store { 97 | public static cache = false; 98 | } 99 | 100 | const store = new TestStore2(); 101 | const events = await store.fetch('event', 12345); 102 | const event = events.data as Event; 103 | 104 | expect(event).to.be.an('object'); 105 | expect(event.getRecordId()).to.equal(12345); 106 | 107 | mockApi({ 108 | name: 'event-1', 109 | url: 'event/12345', 110 | }); 111 | 112 | const events2 = await store.fetch('event', 12345); 113 | const event2 = events2.data as Event; 114 | 115 | expect(events2).to.not.equal(events); 116 | expect(event2).to.be.an('object'); 117 | expect(event2.getRecordId()).to.equal(12345); 118 | }); 119 | 120 | it('should not cache fetch if the response was an jsonapi error', async () => { 121 | mockApi({ 122 | name: 'error', 123 | url: 'event/12345', 124 | }); 125 | 126 | const store = new Store(); 127 | try { 128 | const events = await store.fetch('event', 12345); 129 | expect(true).to.equal(false); 130 | } catch (resp) { 131 | expect(resp.error).to.be.an('array'); 132 | } 133 | 134 | mockApi({ 135 | name: 'event-1', 136 | url: 'event/12345', 137 | }); 138 | 139 | const events2 = await store.fetch('event', 12345); 140 | const event2 = events2.data as Event; 141 | 142 | expect(event2).to.be.an('object'); 143 | expect(event2.getRecordId()).to.equal(12345); 144 | }); 145 | 146 | it('should not cache fetch if the response was an http error', async () => { 147 | mockApi({ 148 | name: 'event-1', 149 | status: 500, 150 | url: 'event/12345', 151 | }); 152 | 153 | const store = new Store(); 154 | try { 155 | const events = await store.fetch('event', 12345); 156 | expect(true).to.equal(false); 157 | } catch (e) { 158 | expect(e).to.be.an('object'); 159 | } 160 | 161 | mockApi({ 162 | name: 'event-1', 163 | url: 'event/12345', 164 | }); 165 | 166 | const events2 = await store.fetch('event', 12345); 167 | const event2 = events2.data as Event; 168 | 169 | expect(event2).to.be.an('object'); 170 | expect(event2.getRecordId()).to.equal(12345); 171 | }); 172 | }); 173 | 174 | describe('fetchAll caching', () => { 175 | it('should cache fetchAll requests', async () => { 176 | mockApi({ 177 | name: 'events-1', 178 | url: 'event', 179 | }); 180 | 181 | const store = new Store(); 182 | const events = await store.fetchAll('event'); 183 | const event = events.data as Array; 184 | 185 | expect(event).to.be.an('array'); 186 | expect(event.length).to.equal(4); 187 | 188 | const events2 = await store.fetchAll('event'); 189 | 190 | expect(events2).to.equal(events); 191 | }); 192 | 193 | it('should clear fetchAll cache on removeAll', async () => { 194 | mockApi({ 195 | name: 'events-1', 196 | url: 'event', 197 | }); 198 | 199 | const store = new Store(); 200 | const events = await store.fetchAll('event'); 201 | const event = events.data as Array; 202 | 203 | expect(event).to.be.an('array'); 204 | expect(event.length).to.equal(4); 205 | 206 | store.removeAll('event'); 207 | 208 | const req2 = mockApi({ 209 | name: 'events-1', 210 | url: 'event', 211 | }); 212 | 213 | const events2 = await store.fetchAll('event'); 214 | expect(events2.data['length']).to.equal(4); 215 | expect(req2.isDone()).to.equal(true); 216 | }); 217 | 218 | it('should ignore fetchAll cache if force is true', async () => { 219 | mockApi({ 220 | name: 'events-1', 221 | url: 'event', 222 | }); 223 | 224 | const store = new Store(); 225 | const events = await store.fetchAll('event'); 226 | 227 | expect(events.data).to.be.an('array'); 228 | expect(events.data['length']).to.equal(4); 229 | 230 | mockApi({ 231 | name: 'events-1', 232 | url: 'event', 233 | }); 234 | 235 | const events2 = await store.fetchAll('event', true); 236 | 237 | expect(events2).to.not.equal(events); 238 | expect(events2.data).to.be.an('array'); 239 | expect(events2.data['length']).to.equal(4); 240 | }); 241 | 242 | it('should ignore fetchAll cache if static cache is false', async () => { 243 | mockApi({ 244 | name: 'events-1', 245 | url: 'event', 246 | }); 247 | 248 | // tslint:disable-next-line:max-classes-per-file 249 | class TestStore2 extends Store { 250 | public static cache = false; 251 | } 252 | 253 | const store = new TestStore2(); 254 | const events = await store.fetchAll('event'); 255 | 256 | expect(events.data).to.be.an('array'); 257 | expect(events.data['length']).to.equal(4); 258 | 259 | mockApi({ 260 | name: 'events-1', 261 | url: 'event', 262 | }); 263 | 264 | const events2 = await store.fetchAll('event'); 265 | 266 | expect(events2).to.not.equal(events); 267 | expect(events2.data).to.be.an('array'); 268 | expect(events2.data['length']).to.equal(4); 269 | }); 270 | 271 | it('should not cache fetchAll if the response was an jsonapi error', async () => { 272 | mockApi({ 273 | name: 'error', 274 | url: 'event', 275 | }); 276 | 277 | const store = new Store(); 278 | try { 279 | const events = await store.fetchAll('event'); 280 | expect(true).to.equal(false); 281 | } catch (resp) { 282 | expect(resp.error).to.be.an('array'); 283 | } 284 | 285 | mockApi({ 286 | name: 'events-1', 287 | url: 'event', 288 | }); 289 | 290 | const events2 = await store.fetchAll('event'); 291 | 292 | expect(events2.data).to.be.an('array'); 293 | expect(events2.data['length']).to.equal(4); 294 | }); 295 | 296 | it('should not cache fetchAll if the response was an http error', async () => { 297 | mockApi({ 298 | name: 'events-1', 299 | status: 500, 300 | url: 'event', 301 | }); 302 | 303 | const store = new Store(); 304 | try { 305 | const events = await store.fetchAll('event'); 306 | expect(true).to.equal(false); 307 | } catch (e) { 308 | expect(e).to.be.an('object'); 309 | } 310 | 311 | mockApi({ 312 | name: 'events-1', 313 | url: 'event', 314 | }); 315 | 316 | const events2 = await store.fetchAll('event'); 317 | 318 | expect(events2.data).to.be.an('array'); 319 | expect(events2.data['length']).to.equal(4); 320 | }); 321 | 322 | it('should reset chache when resseting the store', async () => { 323 | const store = new Store(); 324 | 325 | mockApi({name: 'event-1', url: 'event'}); 326 | await store.fetchAll('event'); 327 | 328 | store.reset(); 329 | 330 | const mockedApi = mockApi({name: 'event-1', url: 'event'}); 331 | await store.fetchAll('event'); 332 | 333 | expect(mockedApi.isDone()).to.equal(true); 334 | }); 335 | }); 336 | }); 337 | -------------------------------------------------------------------------------- /test/network/error-handling.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import * as fetch from 'isomorphic-fetch'; 3 | 4 | // tslint:disable:no-string-literal 5 | 6 | import {config, Record, Store} from '../../src'; 7 | 8 | import mockApi from '../utils/api'; 9 | import {Event, Image, Organiser, Photo, TestStore, User} from '../utils/setup'; 10 | 11 | const baseStoreFetch = config.storeFetch; 12 | 13 | describe('error handling', () => { 14 | beforeEach(() => { 15 | config.fetchReference = fetch; 16 | config.baseUrl = 'http://example.com/'; 17 | }); 18 | 19 | it('should handle network failure', async () => { 20 | const store = new Store(); 21 | 22 | mockApi({ 23 | name: 'events-1', 24 | status: 404, 25 | url: 'event', 26 | }); 27 | 28 | const fetchRes = store.fetchAll('event'); 29 | 30 | return fetchRes.then( 31 | () => expect(true).to.equal(false), 32 | (response) => { 33 | const err = response.error; 34 | expect(err).to.be.an('object'); 35 | expect(err.status).to.equal(404); 36 | expect(err.message).to.equal('Invalid HTTP status: 404'); 37 | }, 38 | ); 39 | }); 40 | 41 | it('should handle invalid responses', async () => { 42 | const store = new Store(); 43 | 44 | mockApi({ 45 | name: 'invalid', 46 | url: 'event', 47 | }); 48 | 49 | const fetchRes = store.fetchAll('event'); 50 | 51 | return fetchRes.then( 52 | () => expect(true).to.equal(false), 53 | (response) => expect(response.error).to.have.all.keys(['name', 'message', 'type']), 54 | ); 55 | }); 56 | 57 | it('should handle api error', async () => { 58 | const store = new Store(); 59 | 60 | mockApi({ 61 | name: 'error', 62 | url: 'event', 63 | }); 64 | 65 | const fetchRes = store.fetchAll('event'); 66 | 67 | return fetchRes.then( 68 | () => expect(true).to.equal(false), 69 | (response) => expect(response.error[0]).to.be.an('object'), 70 | ); 71 | }); 72 | 73 | it('should handle api error on save', async () => { 74 | const store = new Store(); 75 | 76 | const record = new Record({ 77 | title: 'Test', 78 | }, 'event'); 79 | store.add(record); 80 | 81 | mockApi({ 82 | method: 'POST', 83 | name: 'error', 84 | url: 'event', 85 | }); 86 | 87 | const fetchRes = record.save(); 88 | 89 | return fetchRes.then( 90 | () => expect(true).to.equal(false), 91 | (response) => expect(response.error[0]).to.be.an('object'), 92 | ); 93 | }); 94 | 95 | it('should handle api error on remove', async () => { 96 | const store = new Store(); 97 | 98 | mockApi({ 99 | name: 'events-1', 100 | url: 'event', 101 | }); 102 | 103 | const response = await store.fetchAll('event'); 104 | 105 | mockApi({ 106 | method: 'DELETE', 107 | name: 'error', 108 | url: 'event/1', 109 | }); 110 | 111 | const fetchRes = response.data[0].remove(); 112 | 113 | return fetchRes.then( 114 | () => expect(true).to.equal(false), 115 | (resp) => expect(resp.error[0]).to.be.an('object'), 116 | ); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /test/network/headers.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import * as fetch from 'isomorphic-fetch'; 3 | 4 | // tslint:disable:no-string-literal 5 | 6 | import {config, Record, Store} from '../../src'; 7 | 8 | import mockApi from '../utils/api'; 9 | import {Event, Image, Organiser, Photo, TestStore, User} from '../utils/setup'; 10 | 11 | const baseStoreFetch = config.storeFetch; 12 | 13 | describe('headers', () => { 14 | beforeEach(() => { 15 | config.fetchReference = fetch; 16 | config.baseUrl = 'http://example.com/'; 17 | config.defaultHeaders = { 18 | 'X-Auth': '12345', 19 | 'content-type': 'application/vnd.api+json', 20 | }; 21 | }); 22 | 23 | it ('should send the default headers', async () => { 24 | mockApi({ 25 | name: 'events-1', 26 | reqheaders: { 27 | 'X-Auth': '12345', 28 | }, 29 | url: 'event', 30 | }); 31 | 32 | const store = new Store(); 33 | const events = await store.fetchAll('event'); 34 | 35 | expect(events.data).to.be.an('array'); 36 | }); 37 | 38 | it ('should send custom headers', async () => { 39 | mockApi({ 40 | name: 'events-1', 41 | reqheaders: { 42 | 'X-Auth': '54321', 43 | }, 44 | url: 'event', 45 | }); 46 | 47 | const store = new Store(); 48 | const events = await store.fetchAll('event', false, { 49 | headers: { 50 | 'X-Auth': '54321', 51 | }, 52 | }); 53 | 54 | expect(events.data).to.be.an('array'); 55 | }); 56 | 57 | it ('should receive headers', async () => { 58 | mockApi({ 59 | headers: { 60 | 'X-Auth': '98765', 61 | }, 62 | name: 'events-1', 63 | url: 'event', 64 | }); 65 | 66 | const store = new Store(); 67 | const events = await store.fetchAll('event'); 68 | 69 | expect(events.data).to.be.an('array'); 70 | expect(events.headers.get('X-Auth')).to.equal('98765'); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/network/params.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import * as fetch from 'isomorphic-fetch'; 3 | 4 | // tslint:disable:no-string-literal 5 | 6 | import {config, ParamArrayType, Store} from '../../src'; 7 | 8 | import mockApi from '../utils/api'; 9 | import {Event, TestStore} from '../utils/setup'; 10 | 11 | describe('params', () => { 12 | beforeEach(() => { 13 | config.fetchReference = fetch; 14 | config.baseUrl = 'http://example.com/'; 15 | }); 16 | 17 | it('should support basic filtering', async () => { 18 | mockApi({ 19 | name: 'events-1', 20 | query: (q) => expect(q).to.eql({filter: {name: 'foo'}}), 21 | url: 'event', 22 | }); 23 | 24 | const store = new Store(); 25 | const events = await store.fetchAll('event', false, {filter: {name: 'foo'}}); 26 | 27 | expect(events.data).to.be.an('array'); 28 | expect(events.data['length']).to.equal(4); 29 | }); 30 | 31 | it('should support advanced filtering', async () => { 32 | mockApi({ 33 | name: 'events-1', 34 | query: (q) => expect(q).to.eql({filter: {'bar.id': '2', 'name': 'foo'}}), 35 | url: 'event', 36 | }); 37 | 38 | const store = new Store(); 39 | const events = await store.fetchAll('event', false, {filter: {name: 'foo', bar: {id: 2}}}); 40 | 41 | expect(events.data).to.be.an('array'); 42 | expect(events.data['length']).to.equal(4); 43 | }); 44 | 45 | it('should support sorting', async () => { 46 | mockApi({ 47 | name: 'events-1', 48 | query: (q) => expect(q).to.eql({sort: 'name'}), 49 | url: 'event', 50 | }); 51 | 52 | const store = new Store(); 53 | const events = await store.fetchAll('event', false, {sort: 'name'}); 54 | 55 | expect(events.data).to.be.an('array'); 56 | expect(events.data['length']).to.equal(4); 57 | }); 58 | 59 | it('should support advanced sorting', async () => { 60 | mockApi({ 61 | name: 'events-1', 62 | query: (q) => expect(q).to.eql({sort: '-name,bar.id'}), 63 | url: 'event', 64 | }); 65 | 66 | const store = new Store(); 67 | const events = await store.fetchAll('event', false, {sort: ['-name', 'bar.id']}); 68 | 69 | expect(events.data).to.be.an('array'); 70 | expect(events.data['length']).to.equal(4); 71 | }); 72 | 73 | it('should support inclusion of related resources', async () => { 74 | mockApi({ 75 | name: 'events-1', 76 | query: (q) => expect(q).to.eql({include: 'bar'}), 77 | url: 'event', 78 | }); 79 | 80 | const store = new Store(); 81 | const events = await store.fetchAll('event', false, {include: 'bar'}); 82 | 83 | expect(events.data).to.be.an('array'); 84 | expect(events.data['length']).to.equal(4); 85 | }); 86 | 87 | it('should support advanced inclusion of related resources', async () => { 88 | mockApi({ 89 | name: 'events-1', 90 | query: (q) => expect(q).to.eql({include: 'bar,bar.baz'}), 91 | url: 'event', 92 | }); 93 | 94 | const store = new Store(); 95 | const events = await store.fetchAll('event', false, {include: ['bar', 'bar.baz']}); 96 | 97 | expect(events.data).to.be.an('array'); 98 | expect(events.data['length']).to.equal(4); 99 | }); 100 | 101 | it('should support saving with inclusion of related resources', async () => { 102 | const req = mockApi({ 103 | method: 'POST', 104 | name: 'event-1', 105 | query: (q) => expect(q).to.eql({include: 'bar'}), 106 | url: 'events', 107 | }); 108 | 109 | const store = new TestStore(); 110 | const event = store.add({}, Event.type); 111 | await event.save({include: 'bar'}); 112 | expect(event.getRecordId()).to.equal(12345); 113 | expect(req.isDone()).to.equal(true); 114 | }); 115 | 116 | it('should support sparse fields', async () => { 117 | mockApi({ 118 | name: 'events-1', 119 | query: (q) => expect(q).to.eql({fields: {foo: 'name', bar: 'name'}}), 120 | url: 'event', 121 | }); 122 | 123 | const store = new Store(); 124 | const events = await store.fetchAll('event', false, {fields: {foo: 'name', bar: 'name'}}); 125 | 126 | expect(events.data).to.be.an('array'); 127 | expect(events.data['length']).to.equal(4); 128 | }); 129 | 130 | it('should support advanced sparse fields', async () => { 131 | mockApi({ 132 | name: 'events-1', 133 | query: (q) => expect(q).to.eql({fields: {'foo': 'name', 'bar': 'name', 'bar.baz': 'foo,bar'}}), 134 | url: 'event', 135 | }); 136 | 137 | const store = new Store(); 138 | const events = await store.fetchAll('event', false, {fields: { 139 | 'bar': 'name', 140 | 'bar.baz': ['foo', 'bar'], 141 | 'foo': 'name', 142 | }}); 143 | 144 | expect(events.data).to.be.an('array'); 145 | expect(events.data['length']).to.equal(4); 146 | }); 147 | 148 | it('should support raw params', async () => { 149 | mockApi({ 150 | name: 'events-1', 151 | query: (q) => expect(q).to.eql({a: '1', b: '2', c: '3', sort: 'name'}), 152 | url: 'event', 153 | }); 154 | 155 | const store = new Store(); 156 | const events = await store.fetchAll('event', false, { 157 | params: ['a=1', 'b=2', {key: 'c', value: '3'}], 158 | sort: 'name', 159 | }); 160 | 161 | expect(events.data).to.be.an('array'); 162 | expect(events.data['length']).to.equal(4); 163 | }); 164 | 165 | describe('Param array types', () => { 166 | afterEach(() => { 167 | config.paramArrayType = ParamArrayType.COMMA_SEPARATED; 168 | }); 169 | 170 | it('should work with coma separated values', async () => { 171 | mockApi({ 172 | name: 'events-1', 173 | query: (q) => expect(q).to.eql({filter: {a: '1,2', b: '3'}}), 174 | url: 'event', 175 | }); 176 | 177 | config.paramArrayType = ParamArrayType.COMMA_SEPARATED; 178 | const store = new Store(); 179 | const events = await store.fetchAll('event', false, {filter: {a: [1, 2], b: 3}}); 180 | 181 | expect(events.data).to.be.an('array'); 182 | expect(events.data['length']).to.equal(4); 183 | }); 184 | 185 | it('should work with multiple params', async () => { 186 | mockApi({ 187 | name: 'events-1', 188 | query: (q) => expect(q).to.eql({filter: {a: ['1', '2'], b: '3'}}), 189 | url: 'event', 190 | }); 191 | 192 | config.paramArrayType = ParamArrayType.MULTIPLE_PARAMS; 193 | const store = new Store(); 194 | const events = await store.fetchAll('event', false, {filter: {a: [1, 2], b: 3}}); 195 | 196 | expect(events.data).to.be.an('array'); 197 | expect(events.data['length']).to.equal(4); 198 | }); 199 | 200 | it('should work with multiple params', async () => { 201 | mockApi({ 202 | name: 'events-1', 203 | query: (q) => expect(q).to.eql({filter: {'a.0': '1', 'a.1': '2', 'b': '3'}}), 204 | url: 'event', 205 | }); 206 | 207 | config.paramArrayType = ParamArrayType.OBJECT_PATH; 208 | const store = new Store(); 209 | const events = await store.fetchAll('event', false, {filter: {a: [1, 2], b: 3}}); 210 | 211 | expect(events.data).to.be.an('array'); 212 | expect(events.data['length']).to.equal(4); 213 | }); 214 | 215 | it('should work with multiple params', async () => { 216 | mockApi({ 217 | name: 'events-1', 218 | query: (q) => expect(q).to.eql({filter: {a: ['1', '2'], b: '3'}}), 219 | url: 'event', 220 | }); 221 | 222 | config.paramArrayType = ParamArrayType.PARAM_ARRAY; 223 | const store = new Store(); 224 | const events = await store.fetchAll('event', false, {filter: {a: [1, 2], b: 3}}); 225 | 226 | expect(events.data).to.be.an('array'); 227 | expect(events.data['length']).to.equal(4); 228 | }); 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /test/utils/api.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as nodeUrl from 'url'; 4 | 5 | import {constant, isFunction} from 'lodash'; 6 | import * as nock from 'nock'; 7 | 8 | import IDictionary from '../../src/interfaces/IDictionary'; 9 | import {config} from '../../src/NetworkUtils'; 10 | 11 | /** 12 | * Create a stream from a mock file 13 | * 14 | * @param {String} name - Mock name 15 | * @return {Stream} Mock stream 16 | */ 17 | function getMockStream(name: string): fs.ReadStream { 18 | const testPath = path.join(__dirname, `../mock/${name}.json`); 19 | return fs.createReadStream(testPath); 20 | } 21 | 22 | interface IMockArgs { 23 | name?: string; 24 | method?: string; 25 | hostname?: string; 26 | url?: string; 27 | data?: any; 28 | query?: boolean|Function|Object; 29 | responseFn?: Function; 30 | headers?: nock.HttpHeaders; 31 | reqheaders?: IDictionary; 32 | status?: number; 33 | } 34 | 35 | /** 36 | * Prepare a mock API call 37 | * 38 | * @param {object} param - Param object 39 | * @param {String} param.name - Name of the mock API call 40 | * @param {String} [param.method=requestType.READ] - HTTP method to be used 41 | * @param {String} [param.hostname=config.root] - Hostname to be mocked 42 | * @param {String} [param.url='/'] - URL to be mocked 43 | * @param {any} [param.data] - Expected body 44 | * @param {Function} [param.query=true] - Function to be called during the query step 45 | * @param {Function} param.responseFn - Function to be called when response should be sent 46 | * @param {object} [param.headers={'content-type': 'application/vnd.api+json'}] 47 | * HTTP headers to be used in the mock response 48 | * @param {object} [reqheaders={'content-type': 'application/vnd.api+json'}] 49 | * Expected request headers 50 | * @param {Number} status - HTTP status code that should be returned 51 | * @return {undefined} 52 | */ 53 | export default function mockApi({ 54 | name, 55 | method = 'GET', 56 | url = '/', 57 | data, 58 | query = true, 59 | responseFn, 60 | headers = {'content-type': 'application/vnd.api+json'}, 61 | reqheaders = {'content-type': 'application/vnd.api+json'}, 62 | status = 200, 63 | }: IMockArgs): nock.Scope { 64 | const apiUrl = nodeUrl.parse(config.baseUrl); 65 | const hostname = `${apiUrl.protocol}//${apiUrl.hostname}`; 66 | const nockScope = nock(hostname, {reqheaders}).replyContentLength(); 67 | 68 | let mock = nockScope.intercept(apiUrl.pathname + url, method, data) as nock.Interceptor; 69 | 70 | if (query) { 71 | mock = mock.query(query); 72 | } 73 | 74 | return mock.reply(status, () => { 75 | if (isFunction(responseFn)) { 76 | return responseFn(); 77 | } 78 | return [status, getMockStream(name || url)]; 79 | }, headers); 80 | } 81 | -------------------------------------------------------------------------------- /test/utils/setup.ts: -------------------------------------------------------------------------------- 1 | import {computed} from 'mobx'; 2 | 3 | import {IDictionary, Record, Store} from '../../src'; 4 | 5 | // tslint:disable:max-classes-per-file 6 | 7 | export class User extends Record { 8 | public static type: string = 'user'; 9 | 10 | public firstName: string; 11 | public lastName: string; 12 | 13 | @computed get fullName(): string { 14 | return `${this.firstName} ${this.lastName}`; 15 | } 16 | } 17 | 18 | export class Event extends Record { 19 | public static type: string = 'events'; 20 | public static refs = { 21 | image: 'images', 22 | images: 'images', 23 | organisers: 'organisers', 24 | }; 25 | 26 | public name: string; 27 | public organisers: Array; 28 | public images: Array; 29 | public image: Image; 30 | public imagesLinks: IDictionary; 31 | } 32 | 33 | export class Image extends Record { 34 | public static type: string = 'images'; 35 | public static refs = {event: 'events'}; 36 | 37 | public name: string; 38 | public event: Event; 39 | } 40 | 41 | export class Organiser extends User { 42 | public static type = 'organisers'; 43 | public static refs = {image: 'images'}; 44 | 45 | public image: Image; 46 | } 47 | 48 | export class Photo extends Record { 49 | public static type = 'photo'; 50 | public static defaults = { 51 | selected: false, 52 | }; 53 | 54 | public selected: boolean; 55 | } 56 | 57 | export class TestStore extends Store { 58 | public static types = [User, Event, Image, Organiser, Photo]; 59 | 60 | public user: Array; 61 | public events: Array; 62 | public images: Array; 63 | 64 | public organisers: Array; 65 | public photo: Array; 66 | } 67 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "noImplicitAny": false, 7 | "removeComments": false, 8 | "experimentalDecorators": true, 9 | "noLib": false, 10 | "outDir": "../dist", 11 | "inlineSourceMap": true, 12 | "inlineSources": true, 13 | "lib": [ 14 | "dom", 15 | "es5", 16 | "scripthost", 17 | "es2015.promise" 18 | ] 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "test" 23 | ] 24 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "no-string-literal": false, 5 | "array-type": [true, "generic"], 6 | "quotemark": [true, "single", "avoid-escape"], 7 | "variable-name": [true, "ban-keywords"], 8 | "ban-types": [false] 9 | } 10 | } -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalDevDependencies": { 3 | "isomorphic-fetch": "registry:dt/isomorphic-fetch#0.0.0+20170223183302" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /typings/globals/isomorphic-fetch/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/1b30c8e5c2aa0e179a1b82d7f337124721be2aa6/isomorphic-fetch/index.d.ts 3 | interface ForEachCallback { 4 | (keyId: any, status: string): void; 5 | } 6 | 7 | interface Headers { 8 | append(name: string, value: string): void; 9 | delete(name: string): void; 10 | forEach(callback: ForEachCallback): void; 11 | get(name: string): string | null; 12 | has(name: string): boolean; 13 | set(name: string, value: string): void; 14 | } 15 | 16 | declare var Headers: { 17 | prototype: Headers; 18 | new(init?: any): Headers; 19 | } 20 | 21 | interface Blob { 22 | readonly size: number; 23 | readonly type: string; 24 | msClose(): void; 25 | msDetachStream(): any; 26 | slice(start?: number, end?: number, contentType?: string): Blob; 27 | } 28 | 29 | interface Body { 30 | readonly bodyUsed: boolean; 31 | arrayBuffer(): Promise; 32 | blob(): Promise; 33 | json(): Promise; 34 | text(): Promise; 35 | } 36 | 37 | interface RequestInit { 38 | method?: string; 39 | headers?: any; 40 | body?: any; 41 | referrer?: string; 42 | referrerPolicy?: string; 43 | mode?: string; 44 | credentials?: string; 45 | cache?: string; 46 | redirect?: string; 47 | integrity?: string; 48 | keepalive?: boolean; 49 | window?: any; 50 | } 51 | 52 | interface Request extends Object, Body { 53 | readonly cache: string; 54 | readonly credentials: string; 55 | readonly destination: string; 56 | readonly headers: Headers; 57 | readonly integrity: string; 58 | readonly keepalive: boolean; 59 | readonly method: string; 60 | readonly mode: string; 61 | readonly redirect: string; 62 | readonly referrer: string; 63 | readonly referrerPolicy: string; 64 | readonly type: string; 65 | readonly url: string; 66 | clone(): Request; 67 | } 68 | 69 | declare var Request: { 70 | prototype: Request; 71 | new(input: Request | string, init?: RequestInit): Request; 72 | } 73 | 74 | interface ReadableStream { 75 | readonly locked: boolean; 76 | cancel(): Promise; 77 | } 78 | 79 | interface ResponseInit { 80 | status?: number; 81 | statusText?: string; 82 | headers?: any; 83 | } 84 | 85 | interface Response extends Object, Body { 86 | readonly body: ReadableStream | null; 87 | readonly headers: Headers; 88 | readonly ok: boolean; 89 | readonly status: number; 90 | readonly statusText: string; 91 | readonly type: string; 92 | readonly url: string; 93 | clone(): Response; 94 | } 95 | 96 | declare var Response: { 97 | prototype: Response; 98 | new(body?: any, init?: ResponseInit): Response; 99 | } 100 | 101 | interface GlobalFetch { 102 | fetch(input: Request | string, init?: RequestInit): Promise; 103 | } 104 | 105 | interface Window extends GlobalFetch { 106 | } 107 | 108 | declare function fetch(input: Request | string, init?: RequestInit): Promise; 109 | 110 | declare module "isomorphic-fetch" { 111 | namespace _fetch { } 112 | const _fetch: typeof fetch; 113 | export = _fetch; 114 | } 115 | -------------------------------------------------------------------------------- /typings/globals/isomorphic-fetch/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/1b30c8e5c2aa0e179a1b82d7f337124721be2aa6/isomorphic-fetch/index.d.ts", 5 | "raw": "registry:dt/isomorphic-fetch#0.0.0+20170223183302", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/1b30c8e5c2aa0e179a1b82d7f337124721be2aa6/isomorphic-fetch/index.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | --------------------------------------------------------------------------------