Show:
// eslint-disable-next-line ember/no-mixins
/**
 orm model
 @module mixins
 @submodule model
 */

/**

 @example
 import EmberObject from '@ember/object';
 import model, {DS} from 'ember-easy-orm/mixins/model'

 const {attr} = DS;

 export default EmberObject.extend(model, {
        url: '/v1/food',
        init(){
            this._super(...arguments);
            this.model = {
                'name': attr('string'),
                'desc': attr('string'),
                'pic': attr('array'),
                'province_id': attr('string'),
                'city_id': attr('string'),
                'area_id': attr('string'),
                'town_id': attr('string'),
                'country_id': attr('string'),
                'url': attr('string'),
                'host': attr('string'),
                'tag': attr('array'),
                'user': attr({defaultValue: function(){
                    return {name: '', 'gender': ''};
                }})
            };
        }
    })
 */

import { A, isArray } from '@ember/array';
import Mixin from '@ember/object/mixin';
import Evented from '@ember/object/evented';
import EmberObject, { computed } from '@ember/object';
import { merge } from '@ember/polyfills';
import { isBlank, isNone } from '@ember/utils';

import ajax from './ajax';

export const DS = {
  attr(type, hash) {
    if (typeof type === 'object') {
      hash = type;
      type = undefined;
    }

    if (typeof hash === 'object') {
      if (hash.defaultValue !== undefined) {
        return hash.defaultValue;
      }
    }

    switch (type) {
      case 'string':
        return '';
      case 'boolean':
        return true;
      case 'number':
        return 0;
      case 'array':
        return A;
    }

    return null;
  },
};

/**
 mixin in ORM model
 @public
 @class model
 **/
export default Mixin.create(ajax, Evented, {
  /**
   The api host, default is current host
   @property {String} host
   @default  ""
   */
  host: '',

  /**
   The api namespace  like /v1  /v2
   @property {String} namespace
   @default  ""
   */
  namespace: '',

  /**
   The response data business logic root key like: {'code': 0, 'resp':{'user':[]}, 'msg':''}, the resp is is the rootKey
   @property {String} rootKey
   @default  ""
   */
  rootKey: '',

  /**
   The api url. If rootURL ends with slash , the url should not starts with slash
   @property {String} url
   @default  ""
   */
  url: '',

  /**
   The model object primary key
   @property {String} primaryKey
   @default  "_id"
   */
  primaryKey: '_id',

  /**
   The object is for extract response data {user: [], comment:[], avatar: {}}
   @property {Object} displayModel
   @default  null
   */
  displayModel: null,

  /**
   url for find method request, use this method to custome find url
   @method urlForFind
   @default  /host/namespace/?key=params[key]
   @return String
   */
  urlForFind: function () {
    return this.api;
  },

  /**
   url for findOne method request, use this method to custome findOne url
   @method urlForFindOne
   @default  /{host}/{namespace}/{id}
   @return String
   */
  urlForFindOne: function (id) {
    return this.api + '/' + id;
  },
  /**
   url for save method request, use this method to custome create and update url
   @method urlForSave
   @param id object primary key
   @default  /{host}/{namespace}/{id}/
   @return String
   */
  urlForSave: function (id) {
    return id ? this.api + '/' + id : this.api;
  },

  /**
   url for delete method request, use this method to custome delete url
   @method urlForDelete
   @default  /{host}/{namespace}/{id}
   @param id object primary key
   @return String
   */
  urlForDelete: function (id) {
    return id ? this.api + '/' + id : this.api;
  },
  /**
   make api with host, namespace, url
   @method api
   @property {String} api
   @return  {host}{namespace}{url}
   */
  api: computed('host', 'namespace', 'url', function () {
    return this.host + this.namespace + this.url;
  }),
  /**
   save the record to backend when create or update object
   @method save
   @param model model needed to save
   @return  {Promise}
   */
  save: function (model) {
    let self = this,
      primaryKey = this.primaryKey,
      url = this.urlForSave(model[primaryKey], model),
      record = {},
      model_keys = Object.keys(this.model);

    //filter model data
    for (var i = model_keys.length - 1; i >= 0; i--) {
      let key = model_keys[i];
      if (typeof self.model[key] === 'function') {
        if (typeof model[key] === 'object' && !isArray(model[key])) {
          record[key] = JSON.stringify(model[key]);
          continue;
        }
      }

      if (isArray(model[key])) {
        let content = model[key];
        for (let i = 0; i < content.length; i++) {
          if (typeof content[i] === 'object' && content[i]) {
            model[key][i] = JSON.stringify(content[i]);
          }
        }
      }

      record[key] = model[key] !== undefined ? model[key] : self.model[key];
    }

    //check if is new data
    if (model[primaryKey]) {
      record[primaryKey] = model[primaryKey];
      return this.request.put(url, { data: record }).then(
        function (data) {
          // eslint-disable-next-line no-useless-catch
          try {
            return self.saveSerializer(data);
          } catch (e) {
            throw e;
          }
        },
        function (reason) {
          throw reason;
        }
      );
    }

    return this.request.post(url, { data: record }).then(
      function (data) {
        // eslint-disable-next-line no-useless-catch
        try {
          return self.saveSerializer(data);
        } catch (e) {
          throw e;
        }
      },
      function (reason) {
        throw reason;
      }
    );
  },

  /**
   create new model with init options and model property
   @method createRecord
   @param {Object} init init data
   @return Object current model
   */
  createRecord: function (init) {
    let record = EmberObject.create();
    Object.keys(this.model).map((key) => {
      if (typeof this.model[key] === 'function') {
        record.set(key, this.model[key].apply());
      } else {
        record.set(key, this.model[key]);
      }
    });

    if (typeof init === 'object') {
      merge(record, init);
    }

    return record;
  },

  /**
   delete the record from backend
   @method deleteRecord
   @param {Object} model
   @param {Object} data passed to backend server as extra params
   @return  Promise
   */
  deleteRecord: function (model, data) {
    let self = this,
      _id =
        typeof model === 'string' || typeof model === 'number'
          ? model
          : model[this.primaryKey],
      url = this.urlForDelete(_id, data),
      options = data ? { data: data } : {};

    return this.request.delete(url, options).then(
      function (data) {
        // eslint-disable-next-line no-useless-catch
        try {
          return self.deleteSerializer(data);
        } catch (e) {
          throw e;
        }
      },
      function (reason) {
        throw reason;
      }
    );
  },
  /**
   find the records from backend according to params
   @method find
   @param {Object} params query params
   @return  Promise
   */
  find: function (params) {
    let self = this,
      url = this.urlForFind(params),
      filterParams = this._filterParams(params),
      options = filterParams ? { data: filterParams } : {};

    return this.request.get(url, options).then(
      function (data) {
        // eslint-disable-next-line no-useless-catch
        try {
          return self.findSerializer(data);
        } catch (e) {
          throw e;
        }
      },
      function (reason) {
        throw reason;
      }
    );
  },

  /**
   find only one according to primary id
   @method findOne
   @param {String} id primary key
   @param {Object} data query parameter append to url
   @return Promise
   */
  findOne: function (id, data) {
    let url = this.urlForFindOne(id, data),
      self = this,
      options = data ? { data: data } : {};

    return this.request.get(url, options).then(
      function (data) {
        // eslint-disable-next-line no-useless-catch
        try {
          return self.findOneSerializer(data);
        } catch (e) {
          throw e;
        }
      },
      function (reason) {
        throw reason;
      }
    );
  },
  /**
   filter request params
   @private
   @method _filterParams
   @param {Object} params
   @return Object filtered params
   */
  _filterParams: function (params) {
    if (!params) {
      return;
    }
    Object.keys(params).map((k) => {
      if (isBlank(params[k])) {
        delete params[k];
      }
    });
    return params;
  },
  /**
   find serializer
   @method findSerializer
   @param {Object} data response data from backend
   @return serializer data
   */
  findSerializer: function (data) {
    let result = {};
    if (this.displayModel) {
      let objectKeys = Object.keys(this.displayModel);
      for (let i = 0; i < objectKeys.length; i++) {
        let key = objectKeys[i];
        let keyAttr = this.displayModel[key];
        if (keyAttr === 'array') {
          result[key] = this.to_array(data[key]);
          continue;
        }
        if (keyAttr === 'object') {
          result[key] = this.to_object(data[key]);
          continue;
        }
        result[key] = data[key];
      }
      return result;
    }

    //data is null or undefined
    if (isNone(data)) {
      console.warn('findSerializer response data is null');
      return this.to_array();
    }

    // rootKey is empty
    if (!this.rootKey) {
      return this.to_array(data);
    }

    // response data must be array
    if (!isArray(data[this.rootKey])) {
      console.warn(
        'findSerializer parsedData is not array',
        data,
        this.rootKey
      );
      return this.to_array();
    }

    return this.to_array(data[this.rootKey]);
  },
  /**
   serializer for findOne method
   @method findOneSerializer
   @param {Object} data response data from backend
   @return serializer data
   */
  findOneSerializer: function (data) {
    //data is null or undefined
    if (isNone(data)) {
      console.warn('findOneSerializer response data is null');
      return this.to_object();
    }

    // rootKey is empty
    if (!this.rootKey) {
      return data;
    }

    // parsedObject must be object
    if (isNone(data[this.rootKey])) {
      console.warn('findOneSerializer parsedObject is null');
      return this.to_object();
    }

    return this.to_object(data[this.rootKey]);
  },
  to_array: function (data) {
    return A(data || []);
  },
  to_object: function (data) {
    return EmberObject.create(data || {});
  },
  /**
   serializer for save method
   @method saveSerializer
   @param {Object} data response data from backend
   @return serializer data
   */
  saveSerializer: function (data) {
    return data;
  },
  /**
   serializer for delete
   @method deleteSerializer
   @param {Object} data response data from backend
   @return serializer data
   */
  deleteSerializer: function (data) {
    return data;
  },
  init: function () {
    this._super(...arguments);
    if (typeof this.rootKey !== 'string') {
      throw new Error(`rootKey only allow string type, now is ${this.rootKey}`);
    }
  },
});