'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;
}