Source: service/PackagesService.js

'use strict';

/**
 * Plain functionality in service methods.
 * @module service/packages
 * @license MIT
 * @author Kai KRETSCHMANN <kai@kretschmann.consulting>
 */

require('datejs');
const PackageModel = require('../models/package.js');
const LoginModel = require('../models/login.js');

const eventEmitter = require('../utils/eventer').em;

const log4js = require('log4js');
const logger = log4js.getLogger();
logger.level = process.env.LOGLEVEL || /* istanbul ignore next */ 'warn'; // LCOV_EXCL_LINE

/**
 * Helper function getUserObject
 * @method getUserObject
 * @private
 * @param {string} username The textual user name
 * @returns {Promise} user object or error
 */
function getUserObject (username) {
  return new Promise(function (resolve, reject) {
    LoginModel.find({
      name: username
    })
      .then(itemFound => {
        if (itemFound.length === 1) {
          logger.info('Found user');
          resolve(itemFound[0]);
        } else {
          reject(Error('Not found'));
        }
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`getUser failed: ${err}`);
        reject(err);
      });
  });
}

/**
 * Helper function getAggregateObject
 * @method getAggregateObject
 * @private
 * @param {string} grouping The value to group with
 * @returns {Promise} data object or error
 */
function getAggregateObject (grouping) {
  logger.info(`In getAggregateObject group by ${grouping}`);
  return new Promise(function (resolve, reject) {
    let mydata;
    async function docount () {
      logger.info('before await');
      mydata = await PackageModel.aggregate([
        {
          $group: {
            _id: grouping,
            count: { $sum: 1 }
          }
        }]);
      logger.info('after await');
    }
    logger.info('Call docount');
    docount()
      .then(() => {
        logger.info('Did get result');
        replyWithSummary(resolve, mydata);
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error('We have a probleme here: ' + err)
        reject(err);
      });
  });
}

/**
 * Helper function getCountAll
 * @method getCountAll
 * @private
 * @returns {Promise} count object or error
 */
function getCountAll () {
  return new Promise(function (resolve, reject) {
    let count;
    async function docount () {
      count = await PackageModel.estimatedDocumentCount();
      logger.info('might be around ' + count);
    }
    docount()
      .then(() => {
        logger.debug('In then');
        resolve(count);
      })
      .catch(error => /* istanbul ignore next */ {
        logger.error('We have a probleme here: ' + error)
        reject(error);
      });
  });
}

/**
 * Helper function renameAttributes.
 * @method renameAttributes
 * @private
 * @param {Object} item The item object
 * @returns {Object} New item object with renamed properties
 */
function renameAttributes (item) {
  return {
    id: item._id,
    packageName: item.name,
    packageVersion: item.version,
    packageArch: item.arch,
    packageFamily: item.family,
    packageHash: item.hash,
    count: item.count,
    creationDate: item.tscreated
  };
}

/**
 * find a package.
 * @method findPackage
 * @private
 * @param {Function} resolve Positive resolve callback
 * @param {Function} reject Negative resolve callback
 * @param {string} packageName Name of package to find
 * @param {string} packageVersion Version of package to find
 * @param {string} packageArch Architecture of package to find
 * @param {string} packageFamily Family of package to find
 * @returns {Promise} Query result or error object
 */
function findPackage (resolve, reject, packageName, packageVersion, packageArch, packageFamily) {
  PackageModel.find({
    name: packageName,
    version: packageVersion,
    arch: packageArch,
    family: packageFamily
  }, {
    name: 1,
    version: 1,
    arch: 1,
    family: 1,
    hash: 1,
    count: 1,
    tscreated: 1,
    tsupdated: 1
  })
    .then(itemOthers => {
      logger.info('Found others');
      switch (itemOthers.length) {
        case 1:
          resolve(itemOthers);
          break;
        case 0:
          logger.error('Found no match');
          logger.debug(itemOthers);
          reject(new Error('No match'));
          break;
        default:
          logger.error(`Found more then one match ${itemOthers.length}`);
          logger.debug(itemOthers);
          reject(new Error('More then one match'));
      } // switch
    })
    .catch(err => {
      logger.error(`Not found others: ${err}`);
      reject(err);
    });
}

/**
 * Local helper function for error replies
 * @method replyWithError
 * @private
 * @param {Function} reject Negative resolve callback
 * @param {Object} err Error object
 **/
function replyWithError (reject, err) {
  logger.error(`Not OK: ${err}`);
  err.code = 400;
  reject(err);
}

/**
 * Reply with structure
 * @method replyWithSummary
 * @private
 * @param {Function} resolve Positive resolve callback
 * @param {Object} answer Reply structure
 **/
function replyWithSummary (resolve, answer) {
  const tmpStruct = {};

  tmpStruct['application/json'] = {
    summary: answer
  };
  resolve(tmpStruct[Object.keys(tmpStruct)[0]]);
}

/**
 * Validate the package.
 * @method validatePackage
 * @public
 * @param {string} packageName - Name of the package
 * @param {string} packageVersion - Version of the package
 * @param {string} packageArch - Architecture of the package
 * @param {string} packageFamily - Architecture of the package
 * @param {string} _packageSubFamily - Optional subfamily string
 * @param {string} packageHash - SHA hash of the package
 * @param {string} username - The user asking for this, used for creator
 * @returns {Promise} Package data
 **/
exports.validatePackage = function (packageName, packageVersion, packageArch, packageFamily, _packageSubFamily, packageHash, username) {
  return new Promise(function (resolve, reject) {
    logger.info(`In validate service for ${username}`);
    const tsnow = new Date();

    // Does it exist already?
    PackageModel.find({
      name: packageName,
      version: packageVersion,
      arch: packageArch,
      family: packageFamily,
      hash: packageHash
    })
      .then(itemFound => {
        if (itemFound.length === 0) {
          logger.info('Not exist yet');

          getUserObject(username)
            .then(userObject => {
              logger.info(`found ${userObject._id}`);

              const packageNew = new PackageModel({
                name: packageName,
                version: packageVersion,
                creator: userObject,
                arch: packageArch,
                family: packageFamily,
                hash: packageHash,
                tscreated: tsnow,
                tsupdated: tsnow
              });
              packageNew.save()
                .then(itemSaved => {
                  logger.info('Added fresh entry');
                  eventEmitter.emit('putdata', packageName, packageVersion, packageArch, packageFamily, packageHash, true);

                  findPackage(resolve, reject, packageName, packageVersion, packageArch, packageFamily);
                })
                .catch(err => /* istanbul ignore next */ {
                  logger.error(`Not saved fresh: ${err}`);
                  reject(err);
                })
            })
            .catch(err => /* istanbul ignore next */ {
              logger.error(`Some error occured: ${err}`);
            });
        } else {
          logger.info('Did exist already');

          // Get user object by name, just for again validating that parameter, even if we don't use the name for this db entry
          getUserObject(username)
            .then(userObject => {
              logger.info(`found ${userObject._id}`);

              PackageModel.updateOne({
                name: packageName,
                version: packageVersion,
                arch: packageArch,
                family: packageFamily,
                hash: packageHash
              }, {
                $inc: {
                  count: 1
                },
                $set: {
                  tsupdated: tsnow
                }
              }, {
                upsert: true
              })
                .then(_itemUpdated => {
                  logger.info('Did update counter');
                  eventEmitter.emit('putdata', packageName, packageVersion, packageArch, packageFamily, packageHash, false);
                  findPackage(resolve, reject, packageName, packageVersion, packageArch, packageFamily);
                })
                .catch(err => /* istanbul ignore next */ {
                  logger.error(`Not updated: ${err}`);
                  reject(err);
                });
            })
            .catch(err => /* istanbul ignore next */ {
              logger.error(`User not found: ${err}`);
              reject(err);
            });
        } // if
      })
      .catch(err => {
        logger.error(`Query failed: ${err}`);
        reject(err);
      });
  });
};

/**
 * List package data for given combination.
 * @method listPackage
 * @public
 * @param {string} packageName - Name of the package
 * @param {string} packageVersion - Version of the package
 * @param {string} packageArch - Architecture of the package
 * @param {string} packageFamily - Family of the package
 * @returns {Promise} array of entries
 **/
exports.listPackage = function (packageName, packageVersion, packageArch, packageFamily) {
  return new Promise(function (resolve, reject) {
    logger.info('In list service');
    PackageModel.find({
      name: packageName,
      version: packageVersion,
      arch: packageArch,
      family: packageFamily
    }, {
      name: 1,
      version: 1,
      arch: 1,
      family: 1,
      hash: 1,
      count: 1,
      tscreated: 1,
      tsupdated: 1
    })
      .then(item => {
        const r = [];
        item.forEach(function (value) {
          r.push(renameAttributes(value));
        });
        resolve(r);
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`Not OK: ${err}`);
        reject(err);
      });
  });
};

/**
 * List data for a single package.
 * @method listPackageSingle
 * @public
 * @param {string} packageId - ID of the package
 * @returns {Promise} object with entry data
 **/
exports.listPackageSingle = function (packageId) {
  return new Promise(function (resolve, reject) {
    logger.info('In list service');
    PackageModel.find({
      _id: packageId
    }, {
      name: 1,
      version: 1,
      arch: 1,
      family: 1,
      hash: 1,
      count: 1,
      tscreated: 1,
      tsupdated: 1
    })
      .then(item => {
        if (item.length > 0) {
          const r = renameAttributes(item[0]);
          resolve(r);
        } else {
          reject(Error({
            code: 404,
            msg: 'not found'
          }));
        }
      })
      .catch(err => /* istanbul ignore next */ {
        replyWithError(reject, err);
      });
  });
};

/**
 * List all packages with maximum amount and entries to skip.
 * @method listPackages
 * @public
 * @param {number} skip - Skip first replies
 * @param {number} count - Limit replies
 * @param {string} sort - Sort by property
 * @param {string} direction - Sort up or down
 * @param {number} age - maximum age of tsupdated in days to be included
 * @returns {Promise} array of entries
 **/
exports.listPackages = function (skip, count, sort, direction, age) {
  return new Promise(function (resolve, reject) {
    logger.info('In list service');

    let sdir = -1;
    if (direction === 'up') sdir = 1;

    const date = new Date();
    const marginDate = new Date(date.setDate(date.getDate() - age));
    logger.info(`Show from ${marginDate}`);
    PackageModel.find(
      {
        tsupdated: { $gt: marginDate }
      },
      {
        name: 1,
        version: 1,
        arch: 1,
        family: 1,
        hash: 1,
        count: 1,
        tscreated: 1,
        tsupdated: 1
      })
      .sort({
        [sort]: sdir
      })
      .limit(count)
      .skip(skip)
      .then(item => {
        resolve(item);
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`Not OK: ${err}`);
        reject(err);
      });
  });
};

/**
 * List all packages, optimized for UI pagination.
 * @method listPagePackages
 * @public
 * @param {number} page - Skip first replies
 * @param {number} size - Limit replies
 * @param {string} sorters - Sort by property
 * @param {string} _filter - optional filter
 * @returns {Promise} array of entries and number of possible pages with given size value
 **/
exports.listPagePackages = function (page, size, sorters, _filter) {
  return new Promise(function (resolve, reject) {
    logger.info('In listpage service');

    const sdir = -1;
    const iSkip = (page - 1) * size;
    logger.info(`skip=${iSkip}, amount=${size}`);

    getCountAll()
      .then(count => {
        logger.info(`All entries: ${count}`);

        PackageModel.find({}, {
          name: 1,
          version: 1,
          arch: 1,
          family: 1,
          hash: 1,
          count: 1,
          tscreated: 1,
          tsupdated: 1
        })
          .sort({
            [sorters]: sdir
          })
          .limit(size)
          .skip(iSkip)
          .then(item => {
            const iPages = Math.ceil(count / size);
            const resp = {
              last_page: iPages,
              data: item
            };
            resolve(resp);
          })
          .catch(err2 => /* istanbul ignore next */ {
            logger.error(`Not OK: ${err2}`);
            reject(err2);
          });
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error('We have a probleme here: ' + err)
        reject(err);
      })
  });
};

/**
 * List all packages including personal data for admin usage.
 * @method listPackagesFull
 * @public
 * @param {string} count - Limit replies
 * @returns {Promise} array of entries
 **/
exports.listPackagesFull = function (count) {
  return new Promise(function (resolve, reject) {
    logger.info('In list service');
    PackageModel.find({})
      .populate('creator')
      .sort({
        tsupdated: -1
      })
      .limit(count)
      .then(item => {
        resolve(item);
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error(`Not OK: ${err}`);
        reject(err);
      });
  });
};

/**
 * Remove asterisk if any.
 * @method optionalWildcard
 * @private
 * @param {string} searchvalue
 * @returns {string} search string fixed
 */
function optionalWildcard (searchvalue) {
  if (searchvalue.endsWith('*')) {
    return new RegExp('^' + searchvalue.replace('*', ''), 'i');
  } else {
    return searchvalue;
  }
}

/**
 * Internal check in json data.
 * @method checkProp
 * @private
 * @param {json} j
 * @param {string} p
 * @returns boolean
 */
function checkProp (j, p) {
  // eslint-disable-next-line no-prototype-builtins
  return j.hasOwnProperty(p);
}

/**
 * Search for packages by given query.
 * @method searchPackages
 * @public
 * @param {string} jsearch - json query
 * @returns array of entries
 **/
exports.searchPackages = function (jsearch) {
  return new Promise(function (resolve, reject) {
    logger.info('In search packages service');

    const count = 100;
    const queryObj = {};

    if (checkProp(jsearch, 'packageName')) {
      queryObj.name = optionalWildcard(jsearch.packageName);
    }

    if (checkProp(jsearch, 'packageVersion')) {
      queryObj.version = optionalWildcard(jsearch.packageVersion);
    }

    if (checkProp(jsearch, 'packageArch')) {
      queryObj.arch = jsearch.packageArch;
    }
    if (checkProp(jsearch, 'packageFamily')) {
      queryObj.family = jsearch.packageFamily;
    }
    if (checkProp(jsearch, 'packageHash')) {
      queryObj.hash = jsearch.packageHash;
    }
    if (checkProp(jsearch, 'count')) {
      queryObj.count = jsearch.count;
    }
    if (checkProp(jsearch, 'tscreated')) {
      queryObj.tscreated = jsearch.tscreated;
    }
    if (checkProp(jsearch, 'tsupdated')) {
      queryObj.tsupdated = jsearch.tsupdated;
    }
    PackageModel.find(queryObj, {
      name: 1,
      version: 1,
      arch: 1,
      family: 1,
      hash: 1,
      count: 1,
      tscreated: 1,
      tsupdated: 1
    })
      .sort({
        name: 1
      })
      .limit(count)
      .then(item => {
        if (item.length === 0) {
          logger.error('No item found');
          const e = new Error('not found');
          e.msg = 'Not found';
          e.code = 404;
          reject(e);
        } else {
          logger.error('Some item found');
          resolve(item);
        }
      })
      .catch(err => /* istanbul ignore next */ {
        logger.error('Error found');
        replyWithError(reject, err);
      });
  });
};

/**
 * Delete a single entry.
 * @method deleteAndCheckStatus
 * @private
 * @param {object} resolve callback object
 * @param {object} reject callback object
 * @param {string} query for what to delete
 */
function deleteAndCheckStatus (resolve, reject, query) {
  PackageModel.deleteOne(query)
    .then(item => {
      if (item.deletedCount !== 1) {
        logger.error('not found, not deleted');
        reject({
          code: 404,
          msg: 'not found'
        });
      } else {
        resolve('OK');
      }
    })
    .catch(err => /* istanbul ignore next */ {
      replyWithError(reject, err);
    });
}

/**
 * Delete a single package from database.
 * @method deletePackage
 * @public
 * @param {string} packageName - Name of the package
 * @param {string} packageVersion - Version of the package
 * @param {string} packageArch - Architecture of the package
 * @param {string} packageFamily - Family of the package
 * @param {string} packageHash - SHA hash of the package
 * @returns {Promise} OK or error object
 **/
exports.deletePackage = function (packageName, packageVersion, packageArch, packageFamily, packageHash) {
  return new Promise(function (resolve, reject) {
    logger.debug('In deletePackage service');

    const query = {
      name: packageName,
      version: packageVersion,
      arch: packageArch,
      family: packageFamily,
      hash: packageHash
    };
    deleteAndCheckStatus(resolve, reject, query);
  });
};

/**
 * Delete a single package from database.
 * @method deletePackageById
 * @public
 * @param {string} packageId - ID of the package
 * @returns {Promise} OK or error object
 **/
exports.deletePackageById = function (packageId) {
  return new Promise(function (resolve, reject) {
    logger.debug('In deletePackageById service');

    deleteAndCheckStatus(resolve, reject, {
      _id: packageId
    });
  });
};

/**
 * Count number of entries in database.
 * @method countPackage
 * @public
 * @returns {Promise} object with count attribute or error
 **/
exports.countPackage = function () {
  return new Promise(function (resolve, reject) {
    logger.debug('In count service');

    getCountAll()
      .then(count => {
        const tmpS = {};
        tmpS['application/json'] = {
          count
        };
        resolve(tmpS[Object.keys(tmpS)[0]]);
      })
      .catch(error => /* istanbul ignore next */ {
        logger.error('We have a probleme here: ' + error)
        reject(error);
      });
  });
};

/**
 * Check health of system.
 * @method healthCheck
 * @public
 * @returns {Promise} boolean true if all is OK or error
 **/
exports.healthCheck = function () {
  return new Promise(function (resolve, reject) {
    logger.debug('In healthCheck service');

    getCountAll() // just call some cheap function using the DB backend
      .then(_count => {
        resolve(true);
      })
      .catch(error => /* istanbul ignore next */ {
        logger.error('We have a probleme here: ' + error)
        reject(error);
      });
  });
};

/**
 * Count objects per architecture.
 * @method summaryArch
 * @public
 * @returns object with count attribute
 **/
exports.summaryArch = function () {
  logger.debug('In summaryArch service');
  return getAggregateObject('$arch');
};

/**
 * Count objects per family.
 * @method summaryFamily
 * @public
 * @returns object with count attribute
 **/
exports.summaryFamily = function () {
  logger.debug('In summaryFamily service');
  return getAggregateObject('$family');
};

/**
 * Count objects per creator.
 * @method countPerCreator
 * @public
 * @returns array of _id count tuples
 **/
exports.countPerCreator = function () {
  logger.debug('In countPerCreator service');
  return getAggregateObject('$creator');
};

/**
 * export unit testing for test env only
 */
if (process.env.NODE_ENV === 'test') {
  exports.findPackage = findPackage;
}