UNPKG

jii-model

Version:
367 lines (312 loc) 11.6 kB
/** * @author <a href="http://www.affka.ru">Vladimir Kozhin</a> * @license MIT */ 'use strict'; var Jii = require('jii'); var InvalidConfigException = require('jii/exceptions/InvalidConfigException'); var InvalidParamException = require('jii/exceptions/InvalidParamException'); var Collection = require('./Collection'); var Pagination = require('../data/Pagination'); var FetchEvent = require('../model/FetchEvent'); var DataProviderEvent = require('../model/DataProviderEvent'); var _isNumber = require('lodash/isNumber'); var _isArray = require('lodash/isArray'); var _isObject = require('lodash/isObject'); var _isFunction = require('lodash/isFunction'); var _findKey = require('lodash/findKey'); var _has = require('lodash/has'); /** * DataProvider provides a base class that implements the [[DataProviderInterface]]. * * @class Jii.base.DataProvider * @extends Jii.base.Collection */ var DataProvider = Jii.defineClass('Jii.base.DataProvider', /** @lends Jii.base.DataProvider.prototype */{ __extends: Collection, __static: /** @lends Jii.base.DataProvider */{ /** * @event Jii.base.DataProvider#before_fetch * @property {Jii.model.FetchEvent} event */ EVENT_BEFORE_FETCH: 'before_fetch', /** * @event Jii.base.DataProvider#after_fetch * @property {Jii.model.FetchEvent} event */ EVENT_AFTER_FETCH: 'after_fetch', /** * @event Jii.base.DataProvider#loading * @property {Jii.model.FetchEvent} event */ EVENT_LOADING: 'loading', }, /** * @type {string|null} an ID that uniquely identifies the data provider among all data providers. * You should set this property if the same page contains two or more different data providers. * Otherwise, the [[pagination]] and [[sort]] may not work properly. */ id: null, /** * @type {function|Jii.base.Query} */ query: null, /** * @type {boolean} */ autoFetch: true, /** * @type {Jii.data.Sort} */ _sort: null, /** * @type {Jii.data.Pagination|boolean} */ _pagination: null, /** * @type {number|null} */ _totalCount: 0, /** * @type {function[]|null} */ _fetchCallbacks: null, /** * @type {object} */ _fetchedKeys: {}, init() { if (this.autoFetch) { this.fetch(); } }, /** * * @param {boolean} [force] * @return {*} */ fetch(force = false) { // Queue promises when fetch in process if (this._fetchCallbacks !== null) { return new Promise(resolve => { this._fetchCallbacks.push(resolve) }); } if (this.isFetched() && !force) { return Promise.resolve(false); } this.trigger(this.__static.EVENT_BEFORE_FETCH, new FetchEvent({ isLoading: true })); this.trigger(this.__static.EVENT_LOADING, new FetchEvent({ isLoading: true })); this._fetchCallbacks = []; return Promise.resolve() .then(() => { // Query as function if (_isFunction(this.query)) { return this.query(this.getPagination()); } // TODO Query, REST, ... throw new InvalidConfigException('Wrong query format in DataProvider.'); }) .then(data => { // Validate response if (!data) { throw new InvalidParamException('Result data is not object in DataProvider.fetch().'); } if (data.totalCount && !_isNumber(data.totalCount)) { throw new InvalidParamException('Result param "totalCount" must be number in DataProvider.fetch().'); } if (!_isArray(data.models)) { throw new InvalidParamException('Result param "models" must be array in DataProvider.fetch().'); } if (_isNumber(data.totalCount)) { this.setTotalCount(data.totalCount); } // No changes, but fetch if (!this.isFetched() && data.models.length === 0 && this.length === 0) { this.trigger(this.__static.EVENT_FETCHED, this._createEvent({ isFetch: true })); } else { this.setModels(data.models); } // Resolve queue promises after current var callbacks = this._fetchCallbacks; this._fetchCallbacks = null; setTimeout(() => { callbacks.forEach(callback => { callback(data.models); }); }); this.trigger(this.__static.EVENT_AFTER_FETCH, new FetchEvent({ isLoading: false })); this.trigger(this.__static.EVENT_LOADING, new FetchEvent({ isLoading: false })); return data; }); }, isFetched() { if (!this.__super()) { return false; } var pagination = this.getPagination(); if (pagination) { let needFetch = false; pagination.getIndexes().forEach(index => { if (!_has(this._fetchedKeys, index.toString()) && index < this.getTotalCount() - 1) { needFetch = true; } }); return !needFetch; } return true; }, /** * Returns the total number of data models. * When [[pagination]] is false, this returns the same value as [[count]]. * Otherwise, it will call [[prepareTotalCount()]] to get the count. * @returns {number} total number of possible data models. */ getTotalCount() { if (this._pagination === false) { return this.parent ? this.parent.getCount() : this.getCount(); } return this._totalCount; }, /** * Sets the total number of data models. * @param {number} value the total number of data models. */ setTotalCount(value) { this._totalCount = value; if (this._pagination) { this._pagination.totalCount = value; } }, /** * Returns the pagination object used by this data provider. * Note that you should call [[prepare()]] or [[getModels()]] first to get correct values * of [[Pagination.totalCount]] and [[Pagination.pageCount]]. * @returns {Jii.data.Pagination|boolean} the pagination object. If this is false, it means the pagination is disabled. */ getPagination() { if (this._pagination === null) { this.setPagination({}); } return this._pagination; }, /** * Sets the pagination for this data provider. * @param {object|Jii.data.Pagination|boolean} value the pagination to be used by this data provider. * @throws InvalidParamException */ setPagination(value) { if (_isObject(value)) { let config = { className: Pagination, totalCount: this.getTotalCount(), }; if (this.id !== null) { config.pageParam = `${this.id}-page`; config.pageSizeParam = `${this.id}-per-page`; } this._pagination = Jii.createObject(Jii.mergeConfigs(config, value)); } else if (value instanceof Pagination || value === false) { this._pagination = value; } else { throw new InvalidParamException('Only Pagination instance, configuration object or false is allowed.'); } this._pagination.on(Pagination.EVENT_CHANGE, this._onPaginationChange.bind(this)); }, /** * @returns {Jii.data.Sort|boolean} the sorting object. If this is false, it means the sorting is disabled. */ getSort() { if (this._sort === null) { this.setSort({}); } return this._sort; }, /** * Sets the sort definition for this data provider. * @param {object|Jii.data.Sort|boolean} value the sort definition to be used by this data provider. * This can be one of the following: * * - a configuration array for creating the sort definition object. The "class" element defaults * to 'jii\data\Sort' * - an instance of [[Sort]] or its subclass * - false, if sorting needs to be disabled. * * @throws InvalidParamException */ setSort(value) { if (_isObject(value)) { let config = {/*className: Sort*/}; // @todo Sort implementation if (this.id !== null) { config.sortParam = `${this.id}-sort`; } this._sort = Jii.createObject(Jii.mergeConfigs(config, value)); } else if (/*value instanceof Sort ||*/ value === false) { // @todo Sort implementation this._sort = value; } else { throw new InvalidParamException('Only Sort instance, configuration object or false is allowed.'); } }, _change(startIndex, toAdd, toRemove, unique, parentCallParams) { if (parentCallParams) { var pagination = this.getPagination(); if (pagination) { toRemove.forEach(model => { let primaryKey = this._getPrimaryKey(model); var finedKey = _findKey(this._fetchedKeys, key => { return key === primaryKey; }); if (finedKey) { delete this._fetchedKeys[finedKey]; } }); parentCallParams.toAdd.forEach((model, index) => { this._fetchedKeys[index + pagination.getOffset()] = this._getPrimaryKey(model); }); this.refreshFilter(); return; } } this.__super(startIndex, toAdd, toRemove, unique); }, _onPaginationChange() { this.refreshFilter(); if (this.autoFetch) { this.fetch(); } }, _filterModels() { var pagination = this.getPagination(); if (pagination) { if (!this.parent) { throw new InvalidConfigException('DataProvider with pagination need parent collection.'); } return pagination.getIndexes() .map(i => { return this.parent._byId[this._fetchedKeys[i]] || null; }) .filter(model => model !== null); } return this.__super(); }, /** * * @param {object} params * @returns {Jii.model.CollectionEvent} */ _createEvent(params) { params.totalCount = this.getTotalCount(); return new DataProviderEvent(params); }, }); module.exports = DataProvider;