eHealthAfrica/LMIS-Chrome

View on GitHub
app/scripts/services/storage-service.js

Summary

Maintainability
A
3 hrs
Test Coverage
'use strict';

angular.module('lmisChromeApp')
    .factory('storageService', function ($q, $window, utility, collections, pouchStorageService) {

      /**
       *  Global variables used to define table names, with this there will be one
       *  point in the code to add and/or update local storage table names.
       *
       *  table names are matched to the corresponding json file at fixtures
       *  folder that holds data used to pre-fill local storage if it is empty.
       *
       */
      var stockCount = 'stockcount';
      var discardCount = 'discard_count';
      var appConfig = 'app_config';
      var surveyResponse = 'survey_response';
      var ccuBreakdown = 'ccu_breakdown';
      var ccuBreakdown2 = 'ccu_breakdown2';
      var pendingSyncs = 'pending_syncs';
      var locations = 'locations';
       //analytics
      var pageviews = 'pageviews';
      var clicks = 'clicks';
      var exceptions = 'exceptions';
      var analyticsLostRecords = 'analytics_lost_records';

      var FIXTURE_NAMES = utility.values(collections);

      /**
       * Add new table data to the store.
       *
       * @param {string} table - Table name.
       * @param {mixed} data - rows of the table (all values are stored as JSON.)
       * @return {promise|Function|promise|promise|promise|*}
       * @private
       */
      var setData = function (table, data) {
        if(!data.hasOwnProperty('uuid')){
          throw 'data should have a uuid or primary key field.';
        }
        return pouchStorageService.put(table, data)
          .then(function(result) {
            // FIXME: item:710
            return result.id;
          });
      };

      var getData = function(key) {
        return pouchStorageService.allDocs(key);
      };
      /**
       * This function removes a given record with the given uuid from the given
       * tableName and returns True if it was done successfully else rejects
       * with reason why removeData failed.
       *
       * @param tableName
       * @param uuid
       * @returns {promise|Function|promise|promise|promise|*}
       */
      var removeRecordFromTable = function(tableName, uuid){
        return pouchStorageService.get(tableName, uuid)
          .then(function(doc) {
            return pouchStorageService.remove(tableName, uuid, doc._rev);
          });
      };

      /**
       * Remove a table from the store.
       *
       * @param key - Table name.
       * @returns {*|boolean|Array|Promise|string}
       */
      var removeData = function(key) {
        return pouchStorageService.destroy(key);
      };

      /**
       * Clear all data from the storage (will not work on API).
       *
       * @returns {*|boolean|!Promise|Promise}
       */
      var clearStorage = function() {
        var promises = [];
        for(var i in _collections){
          var dbName  = _collections[i];
          promises.push(pouchStorageService.destroy(dbName));
        }
        return $q.all(promises);
      };

      /**
       * Insert new database table row.
       *
       * @param table
       * @param data
       * @returns {Promise}
       */
      var insertData = function(table, data) {
        if(data.hasOwnProperty('uuid')){
          throw 'insert should only be called with fresh record that has not uuid or primary key field.';
        }
        data.uuid = utility.uuidGenerator();
        data.created = data.modified = utility.getDateTime();
        return setData(table, data);
      };

      /**
       * Update database table row.
       *
       * @param table
       * @param data
       * @returns {Promise}
       */
      var updateData = function(table, data, updateDateModified) {
        if(!data.hasOwnProperty('uuid')){
          throw 'update should only be called with data that has UUID or primary key already.';
        }
        if(updateDateModified !== false){
           data.modified = utility.getDateTime();
        }

        // FIXME: This is a workaround to maintain interface compatibility with
        // a previous implementation (which did not try to prevent collisions).
        // Rather than introducing an intermediary `get`, the callee should pass
        // `_rev` itself; it should be considered a first-class citizen as with
        // `uuid/_id`, see item:710.
        return pouchStorageService.get(table, data.uuid)
          .then(function(doc) {
            data._rev = doc._rev;
            return setData(table, data);
          })
          .catch(function() {
            return setData(table, data);
          });
      };

      /**
       *  Encapsulates insert/update database table row operations.
       *
       * @param table
       * @param data
       * @returns {*}
       */
      var saveData = function(table, data) {

        if ((typeof data === 'object') && (data !== null)) {
          if (Object.keys(data).indexOf('uuid') !== -1 && data.uuid.length > 0) {
            return updateData(table, data);
          } else {
            return insertData(table, data);
          }
        } else {
          var deferred = $q.defer();
          deferred.reject(data + ' is null or non-object data.');
          return deferred.promise;
        }

      };

      var getFromTableByKey = function(table, key) {
        key = String(key);//force conversion to string
        return pouchStorageService.get(table, key);
      };

      /**
       * TODO: there must be a better framework way of doing this.
       * this is basically just filter() but the idea is that there are probably ways to pass this
       * to the storage layer to get the filtering done in the db, so make it a separae fn and figure that out later
       */
      var getFromTableByLambda = function (tableName, fn) {
        var deferred = $q.defer();
        var results = [];
        try {
          getData(tableName)
              .then(function (data) {
                results = data.filter(fn);
                deferred.resolve(results);
              }).catch(function (reason) {
                deferred.reject(reason);
              });
        } catch (e) {
          deferred.reject(e);
        } finally {
          return deferred.promise;
        }
      };

      /**
       * This returns an array or collection of rows in the given table name,
       * this collection can not be indexed via key, to get table rows that can
       * be accessed via keys use all() or getData()
       */
      var getAllFromTable = function (tableName) {
        var deferred = $q.defer();
        getData(tableName)
            .then(function (data) {
              var rows = [];
              for (var key in data) {
                rows.push(data[key]);
              }
              deferred.resolve(rows);
            })
            .catch(function (reason) {
              deferred.reject(reason);
            });
        return deferred.promise;
      };

      var validateBatch = function(batch) {
        var now = utility.getDateTime();
        if (!utility.has(batch, 'uuid')) {
          batch.uuid = utility.uuidGenerator();
          batch.created = now;
        }
        batch.modified = now;
        return batch;
      };

      var insertBatch = function(table, batches) {
        if (!angular.isArray(batches)) {
          throw 'batches is not an array';
        }

        var _batches = [];
        for (var i = batches.length - 1; i >= 0; i--) {
          _batches.push(validateBatch(batches[i]));
        }

        return pouchStorageService.bulkDocs(table, _batches);
      };

      var setDatabase = function(table, data) {
        return pouchStorageService.bulkDocs(table, data);
      };

    // Union of fixtures and internal collections
    var _collections = [
      stockCount,
      discardCount,
      appConfig,
      ccuBreakdown,
      ccuBreakdown2,
      pendingSyncs
    ].concat(FIXTURE_NAMES);

    var compactDatabases = function() {
      var promises = [];
      for (var i in _collections) {
        var dbName = _collections[i];
        promises.push(pouchStorageService.compact(dbName));
      }
      return $q.all(promises);
    };

    var viewCleanups = function() {
      var promises = [];
      for (var i in _collections) {
        var dbName = _collections[i];
        promises.push(pouchStorageService.viewCleanup(dbName));
      }
      return $q.all(promises);
    };

      var api = {
        all: getAllFromTable,
        add: setData,
        get: getData,
        removeRecord: removeRecordFromTable,
        remove: removeData,
        clear: clearStorage,
        uuid: utility.uuidGenerator,
        insert: insertData,
        update: updateData,
        save: saveData,
        setDatabase: setDatabase,
        compactDatabases: compactDatabases,
        viewCleanups: viewCleanups,
        where: getFromTableByLambda,
        find: getFromTableByKey,
        insertBatch: insertBatch,
        LOCATIONS: locations,
        APP_CONFIG: appConfig,
        CCU_BREAKDOWN: ccuBreakdown,
        CCU_BREAKDOWN2: ccuBreakdown2,
        DISCARD_COUNT: discardCount,
        PENDING_SYNCS: pendingSyncs,
        STOCK_COUNT: stockCount,
        SURVEY_RESPONSE: surveyResponse,
        PAGE_VIEWS: pageviews,
        CLICKS: clicks,
        EXCEPTIONS: exceptions,
        ANALYTICS_LOST_RECORDS: analyticsLostRecords,
        FIXTURE_NAMES: FIXTURE_NAMES,
        // TODO: remove, see item:751
        _COLLECTIONS: _collections
      };

      return angular.extend(api, collections);
    });