@onehat/data
Version:
JS data modeling package with adapters for many storage mediums.
450 lines (380 loc) • 10.2 kB
JavaScript
/** @module Repository */
import MemoryRepository from './Memory.js';
import _ from 'lodash';
import moment from 'moment';
const LAST_SYNC = 'LAST_SYNC';
/**
* Offline Repositories are MemoryRepositories backed by an offline storage medium.
* Data is initially loaded from the storage medium into memory, and data is synced
* on Add/Edit/Delete operations.
* @extends MemoryRepository
*/
class OfflineRepository extends MemoryRepository {
constructor(config = {}) {
super(...arguments);
const defaults = {
isAutoLoad: true,
isAutoSave: true,
};
_.merge(this, defaults, config);
this._index = null;
}
async initialize() {
this.pauseEvents(); // Queue 'initialize' event from super
await super.initialize(); // Initializes index in _loadFromStorage()
this.resumeEvents(true); // Now fire it!
}
/**
* Helper for load.
* Deletes all data in storage medium.
* @private
*/
async _deleteFromStorage() {
try {
const ids = await this._getIndex(),
total = ids.length,
results = [];
if (_.isNil(ids) || (_.isArray(ids) && !ids.length)) {
return;
}
if (this._storageDeleteMultiple) {
await this._storageDeleteMultiple(ids);
} else {
let i, id;
for (i = 0; i < total; i++) {
id = ids[i];
results.push( this._storageDeleteValue(id) );
}
await Promise.all(results);
}
await this._setIndex([]);
} catch (error) {
if (this.debugMode) {
const msg = error && error.message;
debugger;
}
}
}
/**
* Helper for load.
* Gets initial data from storage medium.
* @return {array} data - Array of rawData objects
* @private
*/
async _loadFromStorage() {
try {
this._index = await this._getIndex();
const ids = this._index,
total = ids && ids.length ? ids.length : 0,
results = [];
if (!ids) {
return [];
}
if (this._storageGetMultiple) {
return await this._storageGetMultiple(ids);
} else {
let i, id;
for (i = 0; i < total; i++) {
id = ids[i];
const value = await this._storageGetValue(id);
results.push(value);
}
return results;
}
} catch (error) {
if (this.debugMode) {
const msg = error && error.message;
debugger;
}
return [];
}
}
/**
* Helper for load.
* Saves data to storage medium from direct load.
* @return {array} data - Array of rawData objects
* @private
*/
async _saveToStorage(entities) {
try {
let i, entity,
total = entities.length,
result,
results = [];
if (this._storageSetMultiple) {
// Prepare entities for multi-storage
const values = {};
_.each(entities, (entity) => {
values[entity.id] = entity.getOriginalData();
});
await this._storageSetMultiple(values);
} else {
for (i = 0; i < total; i++) {
entity = entities[i];
result = this._storageSetValue(entity.id, entity.getOriginalData());
results.push(result);
}
await Promise.all(results);
}
const ids = _.map(entities, (entity) => entity.id);
await this._setIndex(ids);
} catch (error) {
if (this.debugMode) {
const msg = error && error.message;
debugger;
}
}
}
/**
* Helper for save().
* Hook into super._doAdd, so we can save new Entity to storage medium.
* @param {object} entity - Entity
* @return {Promise}
* @private
*/
async _doAdd(entity) {
if (!this.canAdd) {
this.throwError('Adding has been disabled on this repository.');
return;
}
// Get a clone, in case we need to revert back to it later
const clone = entity.clone();
// Attempt to add
super._doAdd(entity);
let storageResult;
try {
storageResult = await this._storageSetValue(entity.id, entity.getOriginalData());
await this._addToIndex(entity.id);
} catch (e) {
// Revert to clone
delete this._keyedEntities[entity.id];
entity.destroy();
this._keyedEntities[clone.id] = clone;
}
return storageResult;
}
/**
* Helper for _doAdd.
* Adds id to index in storage medium.
* @param {string} id - The id to add.
* @private
*/
async _addToIndex(id) {
let index = await this._getIndex();
index.push(id);
index = _.uniq(index);
await this._setIndex(index);
}
/**
* Hook into super._doEdit, so we can save Entity changes to storage medium.
*/
async _doEdit(entity) {
if (!this.canEdit) {
this.throwError('Editing has been disabled on this repository.');
return;
}
// Get a clone, in case we need to revert back to it later
if (entity.isDestroyed) {
return await this._storageDeleteValue(entity.id);
}
const clone = entity.clone();
// Attempt to edit
super._doEdit(entity);
let storageResult;
try {
storageResult = await this._storageSetValue(entity.id, entity.getOriginalData());
} catch (e) {
// Revert to clone
entity.isPersisted = clone.isPersisted;
entity._originalData = clone._originalData;
entity._originalDataParsed = clone._originalDataParsed;
}
return storageResult;
}
/**
* Hook into super._doDelete, so we can delete Entity from storage medium.
*/
async _doDelete(entity) {
if (!this.canDelete) {
this.throwError('Deleting has been disabled on this repository.');
return;
}
// Get a clone, in case we need to revert back to it later
const clone = entity.isDestroyed ? null : entity.clone();
// Attempt to delete
super._doDelete(entity);
let storageResult;
try {
storageResult = await this._storageDeleteValue(entity.id);
await this._deleteFromIndex(entity.id);
} catch (e) {
// try to revert to clone
if (clone) {
this._keyedEntities[clone.id] = clone;
} else {
delete this._keyedEntities[entity.id];
}
}
return storageResult;
}
/**
* Helper for _doDelete.
* Deletes id from index in storage medium.
* @param {string} id - The id to delete.
* @private
*/
async _deleteFromIndex(id) {
let index = await this._getIndex();
_.pull(index, id);
await this._setIndex(index);
}
/**
* Don't do anything with storage medium; just go straight to super._doDelete
* @private
*/
_doDeleteNonPersisted(entity) {
return super._doDelete(entity);
}
/**
* Helper for save.
* Should take the results of the batch operations and handle any errors.
* Executes callback that was passed to save()
* @param {array} results - Promises returned from batch operations
* Executes with one argument of batchOperationResults
* @fires save
* @private
*/
async _finalizeSave(results) {
return Promise.all(results)
.then(() => {
this.isSaving = false;
this.emit('save');
});
}
// ____ __
// / _/___ ____/ /__ _ __
// / // __ \/ __ / _ \| |/_/
// _/ // / / / /_/ / __/> <
// /___/_/ /_/\__,_/\___/_/|_|
/**
* Gets the index from the storage medium.
* @return {array} index - Array of ids.
* @private
*/
async _getIndex() {
// return await this._storageGetValue('index');
let result = await this._storageGetValue('index');
if (!result) {
await this._setIndex([]);
result = [];
}
return result;
}
/**
* Gets all keys from the storage medium within current namespace.
* Mainly used for unit testing.
* @return {array} index - Array of keys.
* @private
*/
async _getKeys() {
if (!this._getAllKeys) {
this.throwError('Storage medium does not support _getAllKeys');
return;
}
const re = new RegExp('^' + this.name + '-');
let keys = await this._getAllKeys();
keys = _.filter(keys, (key) => key.match(re));
keys = _.map(keys, (key) => key.replace(re, ''));
return keys;
}
/**
* Overwrites the index in the storage medium.
* @param {array} index - Array of ids.
* @private
*/
async _setIndex(index) {
if (!_.isEqual(this._index, index)) {
this._index = index;
return await this._storageSetValue('index', index);
}
return;
}
/**
* Gets a value from storage medium.
* @param {string} name - The name of the value
* @return {any} value - The value to retrieve.
* @private
* @abstract
*/
async _storageGetValue(name) {
this.throwError('this._storageGetValue must be implemented by OfflineRepository subclass.');
return;
}
/**
* Sets a value in storage medium.
* @param {string} name - The name of the value
* @param {any} value - The value to save.
* @private
* @abstract
*/
async _storageSetValue(name, value) {
this.throwError('this._storageSetValue must be implemented by OfflineRepository subclass.');
return;
}
/**
* Deletes a value in storage medium.
* @param {string} name - The name of the value
* @private
* @abstract
*/
async _storageDeleteValue(name) {
this.throwError('this._storageDeleteValue must be implemented by OfflineRepository subclass.');
return;
}
/**
* Helper function, in case storage medium
* does not have its own namespacing implementation
* @param {string|array} name - The name or an array of names to get
* @return {string|array} namespacedName - The namespaced name(s)
*/
_namespace(name) {
if (_.isArray(name)) {
const oThis = this;
return _.map(name, (key) => oThis.schema.name + '-' + key);
}
return this.schema.name + '-' + name;
}
/**
* Gets the date when this Repository was last synced with remote
* (when this is the "local" side of at LocalFromRemoteRepository)
* @return {moment} lastSync
*/
async getLastSync() {
const dateStr = await this._storageGetValue(LAST_SYNC);
if (!_.isNil(dateStr)) {
const date = moment(dateStr);
if (date.isValid()) {
return date;
}
}
return null;
}
/**
* Sets the date when this Repository was last synced with remote.
* Used when this is the "local" side of a LocalFromRemoteRepository
* @param {date} lastSync
*/
async setLastSync(date) {
await this._storageSetValue(LAST_SYNC, date);
}
/**
* Clears the date when this Repository was last synced with remote.
* Used when this is the "local" side of a LocalFromRemoteRepository
*/
async clearLastSync() {
await this._storageDeleteValue(LAST_SYNC);
}
};
OfflineRepository.className = 'Offline';
OfflineRepository.type = 'offline';
export default OfflineRepository;