All files Response.ts

98.57% Statements 69/70
95.12% Branches 39/41
100% Functions 10/10
98.51% Lines 66/67
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 2911x                     1x   1x   1x   1x                                                                                                                                                                                                                                                                       104x   104x 104x 104x 104x 104x   104x 88x 16x   15x     15x       15x     104x 104x 104x 104x 104x 104x   104x 104x 66x     104x   104x   104x 9x                       1x 15x 15x 3x     12x 12x 12x   12x 6x     12x     12x 12x   12x                       1x 12x 6x 6x 6x 6x   6x                       1x 6x 12x 12x 47x 47x 3x 1x 1x     2x 2x                               1x 3x         2x   3x   1x  
import {action, computed, extendObservable, IComputedValue, isObservableArray} from 'mobx';
import {IModel} from 'mobx-collection-store';
 
import IDictionary from './interfaces/IDictionary';
import IHeaders from './interfaces/IHeaders';
import IRawResponse from './interfaces/IRawResponse';
import IRequestOptions from './interfaces/IRequestOptions';
import IResponseHeaders from './interfaces/IResponseHeaders';
import * as JsonApi from './interfaces/JsonApi';
 
import {NetworkStore} from './NetworkStore';
import {Record} from './Record';
import {Store} from './Store';
import {flattenRecord, keys} from './utils';
 
import {fetchLink, read} from './NetworkUtils';
 
export class Response {
 
  /**
   * API response data (synced with the store)
   *
   * @type {(IModel|Array<IModel>)}
   * @memberOf Response
   */
  public data?: IModel|Array<IModel>;
 
  /**
   * API response metadata
   *
   * @type {object}
   * @memberOf Response
   */
  public meta?: object;
 
  /**
   * API reslonse links
   *
   * @type {object}
   * @memberOf Response
   */
  public links?: IDictionary<JsonApi.ILink>;
 
  /**
   * The JSON API object returned by the server
   *
   * @type {JsonApi.IJsonApiObject}
   * @memberOf Response
   */
  public jsonapi?: JsonApi.IJsonApiObject;
 
  /**
   * Headers received from the API call
   *
   * @type {IResponseHeaders}
   * @memberOf Response
   */
  public headers?: IResponseHeaders;
 
  /**
   * Headers sent to the server
   *
   * @type {IHeaders}
   * @memberOf Response
   */
  public requestHeaders?: IHeaders;
 
  /**
   * Request error
   *
   * @type {(Array<JsonApi.IError>|Error)}
   * @memberOf Response
   */
  public error?: Array<JsonApi.IError>|Error;
 
  /**
   * First data page
   *
   * @type {Promise<Response>}
   * @memberOf Response
   */
  public first: Promise<Response>; // Handled by the __fetchLink
 
  /**
   * Previous data page
   *
   * @type {Promise<Response>}
   * @memberOf Response
   */
  public prev: Promise<Response>; // Handled by the __fetchLink
 
  /**
   * Next data page
   *
   * @type {Promise<Response>}
   * @memberOf Response
   */
  public next: Promise<Response>; // Handled by the __fetchLink
 
  /**
   * Last data page
   *
   * @type {Promise<Response>}
   * @memberOf Response
   */
  public last: Promise<Response>; // Handled by the __fetchLink
 
  /**
   * Received HTTP status
   *
   * @type {number}
   * @memberOf Response
   */
  public status: number;
 
  /**
   * Related Store
   *
   * @private
   * @type {Store}
   * @memberOf Response
   */
  private __store: Store;
 
  /**
   * Server options
   *
   * @private
   * @type {IRequestOptions}
   * @memberOf Response
   */
  private __options: IRequestOptions;
 
  /**
   * Original server response
   *
   * @private
   * @type {IRawResponse}
   * @memberOf Response
   */
  private __response: IRawResponse;
 
  /**
   * Cache used for the link requests
   *
   * @private
   * @type {IDictionary<Promise<Response>>}
   * @memberOf Response
   */
  private __cache: IDictionary<Promise<Response>> = {};
 
  constructor(response: IRawResponse, store?: Store, options?: IRequestOptions, overrideData?: IModel|Array<IModel>) {
    this.__store = store;
    this.__options = options;
    this.__response = response;
    this.status = response.status;
 
    if (store) {
      this.data = overrideData ? store.add(overrideData) : store.sync(response.data);
    } else if (response.data) {
      // The case when a record is not in a store and save/remove are used
      const resp = response.data;
 
      /* istanbul ignore if */
      if (resp.data instanceof Array) {
        throw new Error('A save/remove operation should not return an array of results');
      }
 
      this.data = overrideData || new Record(flattenRecord(resp.data));
    }
 
    this.meta = (response.data && response.data.meta) || {};
    this.links = (response.data && response.data.links) || {};
    this.jsonapi = (response.data && response.data.jsonapi) || {};
    this.headers = response.headers;
    this.requestHeaders = response.requestHeaders;
    this.error = (response.data && response.data.errors) || response.error;
 
    const linkGetter: IDictionary<IComputedValue<Promise<Response>>> = {};
    Object.keys(this.links).forEach((link: string) => {
      linkGetter[link] = computed(() => this.__fetchLink(link));
    });
 
    extendObservable(this, linkGetter);
 
    Object.freeze(this);
 
    if (this.error) {
      throw this;
    }
  }
 
  /**
   * Replace the response record with a different record. Used to replace a record while keeping the same reference
   *
   * @param {IModel} data New data
   * @returns {Response}
   *
   * @memberOf Response
   */
  @action public replaceData(data: Record): Response {
    const record: Record = this.data as Record;
    if (record === data) {
      return this;
    }
 
    const oldId = data.getRecordId();
    const newId = record.getRecordId();
    const type = record.getRecordType();
 
    if (this.__store) {
      this.__store.remove(type, newId);
    }
 
    data.update(record.toJS());
 
    // TODO: Refactor this to avoid using mobx-collection-store internals
    data['__internal'].id = newId;
    this.__updateStoreReferences(type, oldId, newId);
 
    return new Response(this.__response, this.__store, this.__options, data);
  }
 
  /**
   * Update references in the store
   *
   * @private
   * @param {any} type Record type
   * @param {any} oldId Old redord ID
   * @param {any} newId New record ID
   * @memberof Response
   */
  private __updateStoreReferences(type, oldId, newId) {
    if (this.__store) {
      const modelHash = this.__store['__modelHash'][type];
      const oldModel = modelHash[oldId];
      modelHash[newId] = oldModel;
      delete modelHash[oldId];
 
      this.__updateReferences(oldId, newId);
    }
  }
 
  /**
   * Update models that reference the updated model
   *
   * @private
   * @param {any} oldId Old record ID
   * @param {any} newId new record ID
   * @memberof Response
   */
  private __updateReferences(oldId, newId) {
    this.__store['__data'].map((model) => {
      const keyList = keys(model['__data']);
      keyList.map((key) => {
        const keyId = `${key}Id`;
        if (key in model && keyId in model) {
          if (isObservableArray(model[keyId])) {
            const index = model[keyId].indexOf(oldId);
            Iif (index > -1) {
              model[keyId][index] = newId;
            }
          } else Eif (model[keyId] === oldId) {
            model[keyId] = newId;
          }
        }
      });
    });
  }
 
  /**
   * Function called when a link is beeing fetched. The returned value is cached
   *
   * @private
   * @param {any} name Link name
   * @returns Promise that resolves with a Response object
   *
   * @memberOf Response
   */
  private __fetchLink(name) {
    if (!this.__cache[name]) {
 
      /* istanbul ignore next */
      const link: JsonApi.ILink = name in this.links ? this.links[name] : null;
 
      this.__cache[name] = fetchLink(link, this.__store, this.requestHeaders, this.__options);
    }
    return this.__cache[name];
  }
}