UNPKG

@onehat/data

Version:

JS data modeling package with adapters for many storage mediums.

628 lines (520 loc) 13.2 kB
/** @module Repository */ import AjaxRepository from './Ajax.js'; import qs from 'qs'; import _ from 'lodash'; const nonConditionFilters = [ 'q', 'hydrate', 'fields', 'distinct', 'leftJoinWith', 'innerJoin', 'join', 'where', 'matching', 'groupBy', 'contain', 'order', 'limit', 'page', ]; /** * This class contains overrides of specific functions in * AjaxRepository that are unique to OneBuild. * * @extends AjaxRepository */ class OneBuildRepository extends AjaxRepository { constructor(config = {}) { super(...arguments); const defaults = { isAutoLoad: false, isAutoSave: false, api: { get: 'get', add: 'add', edit: 'edit', delete: 'delete', batchAdd: 'batchAdd', batchEdit: 'batchEdit', batchDelete: 'batchDelete', reorder: 'reorder', duplicate: 'duplicate', getLastModifiedDate: 'getLastModifiedDate', }, methods: { // get: 'POST', }, rootProperty: 'data', successProperty: 'success', totalProperty: 'total', messageProperty: 'message', allowsMultiSort: true, // batchAsSynchronous: true, // Add directly to schema for now // combineBatch: true, // writer: { // type: 'json', // asForm: true, // writeAllFields: true, // }, queryParam: 'q', // General-purpose query/searching parameter allQuery: 'getAll', // For getting all results. Basically, nullifies queryParam on backend }; _.merge(this, defaults, config); /** * @member {boolean} allowsMultiSort - Whether to allow >1 sorter */ this.allowsMultiSort = true; } async initialize() { this.registerEvents([ 'logout', ]); await super.initialize(); } /** * Override parent so we can emit 'logout' event on 401 error * * Helper for _do* save operations. * Fires off axios request to server * @private */ _send(method, url, data, headers) { if (!url) { this.throwError('No url submitted'); return; } if (!this.isOnline) { this.throwError('Offline'); return; } const mergedHeaders = _.merge({ // 'Content-Type': 'application/json', // Stops axios from using 'application/x-www-form-urlencoded' Accept: 'application/json', }, this.headers, headers); const options = { url, method, baseURL: this.api.baseURL, transformResponse: null, headers: mergedHeaders, params: method === 'GET' ? data : null, data: method !== 'GET' ? qs.stringify(data) : null, timeout: this.timeout, }; if (this.debugMode) { console.log('Sending ' + url, options); } this.lastSendOptions = options; return this.axios(options) .catch(error => { if (this.debugMode) { console.log(url + ' error', error); console.log('response:', error.response); } // BEGIN MOD if (error && error.response && error.response.status === 401) { this.emit('logout'); console.log('logout'); return false; } // END MOD this.throwError(error); return; }); } /** * Helper for reloadEntity. * @private */ _getReloadEntityParams(entity) { const params = { conditions: {}, }; params.conditions[entity.schema.name + '.id'] = entity.id; return params; } /** * Sets "conditions" param. * OneBuild uses a single, multi-dimentional param for filtering. * Refreshes entities. */ _onChangeFilters() { // Clear existing "conditions" params if (!_.isEmpty(this._params)) { this._params = _.omitBy(this._params, (value, key) => { return key.match(/^conditions/) || _.includes(nonConditionFilters, key); }); } const oThis = this; _.each(this.filters, (filter, ix) => { if (_.includes(nonConditionFilters, filter.name)) { oThis.setParam(filter.name, filter.value); } else { oThis.setParam('conditions[' + filter.name + ']', filter.value); } }); if (this.isAutoLoad && this.isLoaded && !this.eventsPaused) { if (this.isTree) { return this.loadRootNodes(1); } else { return this.reload(); } } } /** * Sets "order" param. * OneBuild uses a single order param, rather than separate name & direction params. * Refreshes entities. */ _onChangeSorters() { let sorterStrings = []; _.each(this.sorters, (sorter) => { sorterStrings.push(sorter.name + ' ' + sorter.direction); }); if (!_.isEmpty(sorterStrings)) { this.setBaseParam('order', sorterStrings.join(',')); } if (!this.eventsPaused) { if (this.isLoaded && this.isAutoLoad) { if (this.isTree) { return this.loadRootNodes(1).then(() => { this.emit('changeSorters'); }); } else { return this.reload().then(() => { this.emit('changeSorters'); }); } } else { this.emit('changeSorters'); } } } /** * Helper for _send. * Handles server's response to _send(). * This is basically just looking for errors. * @fires error * @private */ _processServerResponse(result) { const retNull = { root: null, success: false, total: 0, message: null, }; if (result === false) { // e.g. 401 error return retNull; } // use try/catch in case the response is not JSON let response; try { response = _.isPlainObject(result.data) ? result.data : this.reader.read(result.data); } catch(e) { return retNull; } const root = response[this.rootProperty], success = response[this.successProperty], total = response[this.totalProperty], message = response[this.messageProperty]; if (message === 'You do not have authorization to access this area.') { this.emit('logout'); } if (!success) { this.throwError(message, root); } return { root, success, total, message }; } /** * Integrates with RestTrait::reorder in OneBuild API * @param {entity|array} dragRecordOrIds - which entity or ids were being dragged * @param {entity} dropRecord - which entity it was dropped on to * @param {string} dropPosition - position in which it was dropped; could be 'before' or 'after' * @return {Promise} */ reorder(dragRecordOrIds, dropRecord, dropPosition) { if (!this.isOnline) { this.throwError('Offline'); return; } let ids; if (_.isArray(dragRecordOrIds)) { ids = dragRecordOrIds; } else if (dragRecordOrIds?.id) { ids = [dragRecordOrIds.id]; } else { throw Error('dragRecordOrIds must be an entity or array of ids') } const data = { url: this.getModel() + '/' + this.api.reorder, data: qs.stringify({ ids, dropPosition, dropRecord_id: dropRecord.id, }), method: 'POST', baseURL: this.api.baseURL, }; if (this.debugMode) { console.log('reorder', data); } return this.axios(data) .then((result) => { if (this.debugMode) { console.log('reorder response', result); } const response = result.data; if (!response.success) { this.throwError(response.data); return; } // Reload the repository, so updated sort_order values can be retrieved this.reload(); }); } async remoteDuplicate(entity) { this.markLoading(); const url = entity.getModel() + '/' + this.api.duplicate, id = entity.id, result = await this._send('POST', url, { id }); if (!result) { this.markLoading(false); this.throwError('error duplicating on server'); return; } const { root, success, total, message } = this._processServerResponse(result); if (!success) { this.markLoading(false); this.throwError(message); return; } // Click duplicateId. The new row appears directly above the one that was duplicated // The new duplicate is selected // The display field should read "{old name} (duplicate)" const duplicateEntity = await this.createStandaloneEntity(root, true, true); if (entity.isRemotePhantomMode) { entity.isRemotePhantom = true; } this._insertBefore(duplicateEntity, entity); this.markLoading(false); return duplicateEntity; } async loadOneAdditionalEntity(id) { const entity = await this.getSingleEntityFromServer(id); if (!entity) { this.throwError('entity not found'); return; } this._relayEntityEvents(entity); this.entities.push(entity); this.total++; this._setPaginationVars(); this.emit('changeData', this.entities); return entity; } async getSingleEntityFromServer(id) { if (this.isDestroyed) { this.throwError('this.getSingleEntityFromServer is no longer valid. Repository has been destroyed.'); return; } if (!this.api.get) { this.throwError('No "get" api endpoint defined.'); return; } if (!id) { return null; } this.markLoading(); const idPropertyName = this.getSchema().model.idProperty; const params = {}; params['conditions[' + idPropertyName + ']'] = id; const url = this.getModel() + '/' + this.api.get, data = _.merge(params, this._baseParams); if (this.debugMode) { console.log('getSingleEntityFromServer', data); } return this._send(this.methods.get, url, data) .then(result => { if (this.debugMode) { console.log('Response for getSingleEntityFromServer for ' + this.name, result); } if (this.isDestroyed) { // If this repository gets destroyed before it has a chance // to process the Ajax request, just ignore the response. return; } const { root, success, total, message } = this._processServerResponse(result); if (!root[0]) { return null; } const entity = this.createStandaloneEntity(root[0]); entity.isPersisted = true; entity.isRemotePhantom = false; return entity; }) .finally(() => { this.markLoading(false); }); } async getLastModifiedDate() { if (this.isDestroyed) { this.throwError('this.getLastModifiedDate is no longer valid. Repository has been destroyed.'); return; } if (!this.api.getLastModifiedDate) { this.throwError('No "getLastModifiedDate" api endpoint defined.'); return; } this.markLoading(); if (this.debugMode) { console.log('getLastModifiedDate'); } const url = this.getModel() + '/' + this.api.getLastModifiedDate; return this._send(this.methods.get, url, this._baseParams) .then(result => { if (this.debugMode) { console.log('Response for getLastModifiedDate for ' + this.name, result); } if (this.isDestroyed) { // If this repository gets destroyed before it has a chance // to process the Ajax request, just ignore the response. return; } const { root, success, total, message } = this._processServerResponse(result); if (!success) { this.throwError(message); return; } return root; }) .finally(() => { this.markLoading(false); }); } /** * Login to OneBuild API * @param {object} creds - object with two properties: * - username, * - password, * @return {Promise} */ login(creds) { if (!this.isOnline) { this.throwError('Offline'); return; } const data = { url: 'apiLogin', data: qs.stringify(creds), method: 'POST', baseURL: this.api.baseURL, }; if (this.debugMode) { console.log('login', data); } return this.axios(data) .then((result) => { if (this.debugMode) { console.log('login response', result); } const response = result.data; if (!response.success) { this.throwError(response.data); // TODO: Fix back-end, so OneBuild submits the error message on response.message, not response.data return false; } const userData = response.data; return userData; }); } /** * Logout from OneBuild API * @return {Promise} */ logout() { if (!this.isOnline) { this.throwError('Offline'); return; } const data = { url: 'Users/apiLogout', method: 'POST', baseURL: this.api.baseURL, headers: _.merge({ 'Content-Type': 'application/json', Accept: 'application/json', }, this.headers), timeout: this.timeout, }; if (this.debugMode) { console.log('logout', data); } return this.axios(data) .then((result) => { if (this.debugMode) { console.log('logout response', result); } const response = result.data; if (!response.success) { this.throwError(response.data); return; } return true; }); } forgotPassword(email = null, username = null) { if (!this.isOnline) { this.throwError('Offline'); return; } const data = { url: 'forgotPassword', data: qs.stringify({ email, username, }), method: 'POST', baseURL: this.api.baseURL, }; if (this.debugMode) { console.log('forgotPassword', data); } return this.axios(data) .then((result) => { if (this.debugMode) { console.log('forgotPassword response', result); } const response = result.data; if (!response.success) { this.throwError(response.data); return; } return response; }); } } OneBuildRepository.className = 'OneBuild'; OneBuildRepository.type = 'onebuild'; export default OneBuildRepository;