'use strict';
const util = require('util');
const _ = require('lodash');
const Loki = require('lokijs');
const HatAdapter = require('./hat-adapter');
const Hat = require('../hat');
const COLLECTION = {
USER: 'user',
GROUP: 'group',
PERMISSION: 'permission'
};
/**
* @classdesc Representing an hat adapter class for LokiJS
* @class
* @extends HatAdapter
* @param {object} [logger] - Logger instance
*/
function LokiJSHatAdapter(logger) {
HatAdapter.call(this);
Object.defineProperty(this, 'EVENT', {
writable: true,
enumerable: true,
configurable: true,
value: {
ERROR: 'error',
EVENT: 'event',
WARNING: 'warning',
CLOSE: 'close',
LOADED: 'loaded'
}
});
Object.defineProperty(this, 'logger', {
writable: true,
enumerable: true,
configurable: false,
value: logger || console
});
Object.defineProperty(this, 'dbCollection', {
writable: true,
enumerable: true,
configurable: false,
value: {}
});
Object.defineProperty(this, 'db', {
writable: true,
enumerable: true,
configurable: false,
value: null
});
}
util.inherits(LokiJSHatAdapter, HatAdapter);
/**
* This gets a collection when not set, it will be created
* @param {string} collectionName - document collection to be initialized
* @private
*/
LokiJSHatAdapter.prototype._initializeCollection = function (collectionName) {
this.dbCollection[collectionName] = this.db.getCollection(collectionName);
if (!this.dbCollection[collectionName]) {
this.dbCollection[collectionName] = this.db.addCollection(collectionName, {
unique: ['name'],
indices: ['name']
});
}
// Receive events from lokiJS and pass them along:
this.dbCollection[collectionName].on('warning', (warning) => {
this.logger.warn(warning);
this.emit(this.EVENT.WARNING, warning);
});
this.dbCollection[collectionName].on('error', (errDoc) => {
this.logger.error(errDoc);
this.emit(this.EVENT.ERROR, errDoc);
});
this.dbCollection[collectionName].on('close', () => {
this.emit(this.EVENT.CLOSE);
});
this.dbCollection[collectionName].on('loaded', (loaded) => {
this.emit(this.EVENT.LOADED, loaded);
});
};
/**
* Generic Upsert method for user, group and permission collections
*
* @param {string} collection - Dynamic pointer to a collection
* @param {object} criteria - In this adapter name is our unique reference for all collections
* @param {object} data - Object to update or insert
* @param {function} callback
* @private
*/
LokiJSHatAdapter.prototype._upsertByName = function (collection, criteria, data, callback) {
let dataFound = this.dbCollection[collection].findOne({name: criteria.name});
if (!dataFound) {
dataFound = this.dbCollection[collection].insert(data);
} else {
dataFound = _.merge(dataFound, data);
try {
this.dbCollection[collection].update(dataFound);
} catch (error) {
return callback(error);
}
}
return callback(null, dataFound);
};
LokiJSHatAdapter.prototype._findByName = function (name, criteria, callback) {
const found = this.dbCollection[name].findOne(criteria);
callback(null, found);
};
/**
* Initialize persistence storage itself and it's collections
* @override
* @param {function} callback
*/
LokiJSHatAdapter.prototype.initialize = function (callback) {
const self = this;
this.db = new Loki('lokijs.db', {
autosave: false,
autoload: true,
verbose: true,
autoloadCallback: loadHandler,
});
function loadHandler() {
// Initialize all collections
self._initializeCollection(COLLECTION.USER);
self._initializeCollection(COLLECTION.GROUP);
self._initializeCollection(COLLECTION.PERMISSION);
callback();
}
};
/**
* Tries to fetch a user
* @override
* @param {object} criteria - LokiJS criteria
* @param {function} callback
*/
LokiJSHatAdapter.prototype.findUser = function (criteria, callback) {
this._findByName(COLLECTION.USER, criteria, callback);
};
LokiJSHatAdapter.prototype.findGroup = function (criteria, callback) {
this._findByName(COLLECTION.GROUP, criteria, callback);
};
LokiJSHatAdapter.prototype.findPermission = function (criteria, callback) {
this._findByName(COLLECTION.PERMISSION, criteria, callback);
};
/**
* Add a user object to the user collection
* @param {object} user - The user object
* @param {function} callback
*/
LokiJSHatAdapter.prototype.addUser = function (user, callback) {
this.dbCollection[COLLECTION.USER].insert(user);
callback(null, user);
};
/**
* Add a group object to the group collection
* @param {object} group - The group object
* @param {function} callback
*/
LokiJSHatAdapter.prototype.addGroup = function (group, callback) {
this.dbCollection[COLLECTION.GROUP].insert(group);
callback(null, group);
};
/**
* Add a groups from array to the group collection
* @param {Array} groups - The group object
* @param {function} callback
*/
LokiJSHatAdapter.prototype.addGroups = function (groups, callback) {
if (!(Array.isArray(groups))) {
return callback(new TypeError('groups not an instance of array'));
}
const groupsAdded = [];
for (let i = 0; i < groups.length; i++) {
const group = groups[i];
const groupAdded = this.dbCollection[COLLECTION.GROUP].insert(group);
groupsAdded.push(groupAdded);
}
callback(null, groupsAdded);
};
/**
* Add a permission object to the permission collection
* @param {object} permission - The permission object
* @param {function} callback
*/
LokiJSHatAdapter.prototype.addPermission = function (permission, callback) {
this.dbCollection[COLLECTION.PERMISSION].insert(permission);
callback(null, permission);
};
/**
* Add a permission from array to the permission collection
* @param {Array} permissions - The permission object
* @param {function} callback
*/
LokiJSHatAdapter.prototype.addPermissions = function (permissions, callback) {
if (!(Array.isArray(permissions))) {
return callback(new TypeError('permissions not an instance of array'));
}
const permissionsAdded = [];
for (let i = 0; i < permissions.length; i++) {
const permission = permissions[i];
const permissionAdded = this.dbCollection[COLLECTION.PERMISSION].insert(permission);
permissionsAdded.push(permissionAdded);
}
callback(null, permissionsAdded);
};
/**
* Remove a user from the user collection
* @param user
* @param callback
* @returns {*}
*/
LokiJSHatAdapter.prototype.removeUser = function (user, callback) {
try {
this.dbCollection[COLLECTION.USER].removeWhere({name: user.name});
} catch (error) {
return callback(error);
}
callback();
};
/**
* Remove a group from the group collection
* @param group
* @param callback
* @returns {*}
*/
LokiJSHatAdapter.prototype.removeGroup = function (group, callback) {
try {
this.dbCollection[COLLECTION.GROUP].removeWhere({name: group.name});
} catch (error) {
return callback(error);
}
callback();
};
/**
* Remove a permission from the permission collection
* @param permission
* @param callback
* @returns {*}
*/
LokiJSHatAdapter.prototype.removePermission = function (permission, callback) {
try {
this.dbCollection[COLLECTION.PERMISSION].removeWhere({name: permission.name});
} catch (error) {
return callback(error);
}
callback();
};
/**
* Insert or update a user (if not exists)
* @override
* @param {object} criteria - Where to find in case of update
* @param {object} user - The user to update or insert
* @param {function} callback
*/
LokiJSHatAdapter.prototype.upsertUser = function (criteria, user, callback) {
this._upsertByName(COLLECTION.USER, criteria, user, callback);
};
/**
* Insert or update group (if not exists)
* @override
* @param {object} criteria - LokiJS criteria
* @param {Hat} group
* @param callback
*/
LokiJSHatAdapter.prototype.upsertGroup = function (criteria, group, callback) {
this._upsertByName(COLLECTION.GROUP, criteria, group, callback);
};
/**
* Insert or update permissions (if not exists)
* @override
* @param {object} criteria - LokiJS criteria
* @param permission
* @param callback
*/
LokiJSHatAdapter.prototype.upsertPermission = function (criteria, permission, callback) {
this._upsertByName(COLLECTION.PERMISSION, criteria, permission, callback);
};
/**
* Tries to fetch a collection of Hats representing groups
* @override
* @param {object} user - user object
* @param callback
*/
LokiJSHatAdapter.prototype.findGroupsByUser = function (user, callback) {
// Fetch user from lokijs
const userFound = this.dbCollection[COLLECTION.USER].findOne({name: user.name});
if (!userFound) {
return callback(new Error('user not found'));
}
// construct a new Hat instance from encoded groups
const hat = Hat.fromString(userFound.groups);
// Fetch lokijs groups from group collection
const positions = hat.getPositionsArray();
const criteria = {'$loki': {'$in': positions}};
const groups = this.dbCollection[COLLECTION.GROUP].find(criteria);
return callback(null, groups);
};
/**
* Given a user, it tries to fetch it's permissions
* @param {object} user - Instance of a user object
* @param {function} callback
*/
LokiJSHatAdapter.prototype.findPermissionsByUser = function (user, callback) {
const self = this;
let permissions = [];
this.findGroupsByUser(user, (err, groups) => {
if (err) {
return callback(err);
}
let i = 0;
for (i; i < groups.length; i++) {
const group = groups[i];
const permissionsFromGroup = group.permissions;
const hat = Hat.fromString(permissionsFromGroup);
const permissionBitPositions = hat.getPositionsArray();
permissions = permissions.concat(permissionBitPositions);
permissions = _.uniq(permissions);
}
permissions = permissions.sort(function sortNumber(a, b) {
return a - b;
});
const criteria = {'$loki': {'$in': permissions}};
const permissionsFound = self.dbCollection[COLLECTION.PERMISSION].find(criteria);
return callback(null, permissionsFound);
});
};
/**
* Validate a user against all given permissions <br>
* When one of the given permissions is missing for the given user,<br>
* given user is not valid.
*
* @param {object} user - User to validate
* @param {Array<Number>} permissions - Array with unsigned integers with permissions (positions starting at 1)
* @param {function} callback
*/
LokiJSHatAdapter.prototype.validateUserWithAllPermissions = function (user, permissions, callback) {
let valid = false;
this.findPermissionsByUser(user, (error, permissionsFound) => {
if (error) {
return callback(error, valid);
}
if (!(Array.isArray(permissions)) || permissions.length <= 0) {
return callback(null, valid);
}
const permissionsAttachedToUser = _.map(permissionsFound, '$loki');
const missingPermissions = _.difference(permissions, permissionsAttachedToUser);
valid = !(Array.isArray(missingPermissions) && missingPermissions.length > 0);
callback(null, valid);
});
};
/**
* Validate a user against any given permissions <br>
* When one of the given permissions is found for the given user,<br>
* the outcome is valid.
*
* @param {object} user - User to validate
* @param {Array<Number>} permissions - Array with unsigned integers with permissions (positions starting at 1)
* @param {function} callback
*/
LokiJSHatAdapter.prototype.validateUserWithAnyPermissions = function (user, permissions, callback) {
let valid = false;
this.findPermissionsByUser(user, (error, permissionsFound) => {
if (error) {
return callback(error, valid);
}
if (!(Array.isArray(permissions)) || permissions.length <= 0) {
return callback(null, valid);
}
const permissionsAttachedToUser = _.map(permissionsFound, '$loki');
const overlappingPermissions = _.intersection(permissions, permissionsAttachedToUser);
valid = (Array.isArray(overlappingPermissions) && overlappingPermissions.length > 0);
callback(null, valid);
});
};
module.exports = LokiJSHatAdapter;