import { Graph as AncientGraph } from '../graph.js';
import lodash from 'lodash';
import { EventEmitter } from 'fbemitter';
/**
* This method allows you to use ObjectGraph class to its inheritance chain.
*
* @param {Class} ParentClassGraph
* @return {Class} ObjectGraph
*/
function factoryObjectGraph(ParentClassGraph) {
/**
* Inherited class. Class with methods for control links in graph.
* Adapted for array collection.
*
* @class
* @description `import { ObjectGraph as Graph } from 'ancient-graph';`
*/
class ObjectGraph extends ParentClassGraph {
/**
* Construct new graph and checks for required adaptation methods.
* @param {Array[]} collection
* @param {Object.<string, string>} fields - matching of fields in the link with fields in document
* @param {Object} [config] - Additional config.
* @param {Object} [config.aliases]
* @param {String} [config.aliases.$]
* @throws {Error} if the adapter methods is not complete
*/
constructor(collection, fields, config) {
super(...arguments);
this.emitter = new EventEmitter();
}
/**
* Specifies the id field on insert
*
* @param {number} index
* @param {Link} link
* @return {number|string} id;
*/
_idGenerator(index, link) {
return ""+index;
};
/**
* Generate insert modifier.
*
* @param {number} index
* @param {Link} link
* @return {number|string} id;
*/
_insertModifier(link) {
var _modifier = {};
for (var f in link) {
if (this.fields[f]) {
_modifier[this.fields[f]] = link[this.config.aliases[f]];
}
}
return _modifier;
};
/**
* Should insert new link into graph.
* Return a synchronous result. This can be useful in your application. But for writing generic code, it is recommended to only use the callback result.
*
* @param {Link} link
* @param {Graph~insertCallback} [callback]
* @param {Object} [context]
* @return {number|string} [id]
*/
insert(link, callback, context) {
this.callback
var _modifier = this._insertModifier(link);
var index, error, id;
try {
index = this.collection.push(_modifier) - 1;
if (!this.collection[index].hasOwnProperty(this.fields[this.config.aliases['id']])) {
id = this.collection[index][this.fields[this.config.aliases['id']]] = this._idGenerator(index, this.collection[index]);
}
this.emitter.emit('insert', this.collection[index]);
} catch(_error) {
error = _error;
}
if (callback) {
callback(error, id)
}
return id;
}
/**
* Optional callback. If present, called with an error object as the first argument and, if no error, the unique id of inserted link as the second.
*
* @callback Graph~insertCallback
* @param {Error} [error]
* @param {number|string} [id]
*/
/**
* Push into link value some item/items.
*
* @param {Array} data
* @param {string|number|string[]|number[]} item
*/
_updateModifierPush(data, item) {
data.push(item);
}
/**
* Push into link value some item/items if not already exists.
*
* @param {Array} data
* @param {string|number|string[]|number[]} item
*/
_updateModifierAdd(data, item) {
if (lodash.isArray(item)) {
for (var i in item) {
this._updateModifierAdd(data, item[i]);
}
} else {
var index = lodash.indexOf(data, item);
if (index < 0) {
this._updateModifierPush(data, item);
}
}
}
/**
* Remove from link value some item/items.
*
* @param {Array} data
* @param {string|number|string[]|number[]} item
*/
_updateModifierRemove(data, item) {
if (lodash.isArray(item)) {
for (var i in item) {
this._updateModifierRemove(data, item[i]);
}
} else {
lodash.remove(data, function(value) {
return value == item;
});
}
}
/**
* Generate update modifier.
*
* @param {LinkModifier} modifier
* @param {Object} result
* @return {number|string} id;
*/
_updateModifier(modifier, result) {
for (var m in modifier) {
if (this.fields[m]) {
if (typeof(modifier[m]) == 'undefined') {
delete result[this.fields[m]];
} else {
if (typeof(modifier[m]) == 'object') {
if (lodash.isArray(modifier[m])) {
result[this.fields[m]] = modifier[m];
} else {
if (!lodash.isArray(result[this.fields[m]])) {
result[this.fields[m]] = result[this.fields[m]]?[result[this.fields[m]]]:[];
}
for (var key in modifier[m]) {
if (key == 'add') {
this._updateModifierAdd(result[this.fields[m]], modifier[m][key]);
}
if (key == 'push') {
this._updateModifierPush(result[this.fields[m]], modifier[m][key]);
}
if (key == 'remove') {
this._updateModifierRemove(result[this.fields[m]], modifier[m][key]);
}
}
}
} else {
result[this.fields[m]] = modifier[m];
}
}
}
}
};
/**
* Should update to new state of modifier object link by unique id or by link query object.
* If the database allows, it is recommended to return a synchronous result. This can be useful in your application. But for writing generic code, it is recommended to only use the callback result.
*
* @param {string|LinkSelector} selector
* @param {LinkModifier} modifier
* @param {Graph~updateCallback} [callback]
* @param {Object} [context]
* @return {number} [count]
*/
update(selector, modifier, callback, context) {
var results = this._fetch(selector);
for (var r in results) {
var oldResult = lodash.cloneDeep(results[r]);
this._updateModifier(modifier, results[r]);
this.emitter.emit('update', results[r], oldResult);
}
if (callback) callback(undefined, results.length);
return results.length;
}
/**
* Optional callback. If present, called with an error object as the first argument and, if no error, the number of affected documents as the second.
*
* @callback Graph~updateCallback
* @param {Error} [error]
* @param {number} [count]
*/
/**
* Should remove link by unique id or by link query object.
*
* @param {string|LinkSelector} selector
* @param {Graph~removeCallback} [callback]
* @param {Object} [context]
*/
remove(selector, callback, context) {
var oldLength = this.collection.length;
var removed = [];
lodash.remove(this.collection, (result) => {
var _query = this.query(selector)(result);
if (_query) removed.push(result)
return _query;
});
for (var r in removed) {
this.emitter.emit('remove', removed[r]);
}
var newLength = this.collection.length;
if (callback) callback(undefined, oldLength - newLength);
}
/**
* Optional callback. If present, called with an error object as the first argument.
*
* @callback Graph~removeCallback
* @param {Error} [error]
* @param {number} [count]
*/
/**
* Generate adapter for database query for links search by unique id or by link query object.
*
* @param {string|LinkSelector} selector
* @return {*} query
*/
query(selector) {
var type = typeof(selector);
if (type == 'string' || type == 'number') {
return (doc) => { return doc[this.fields[this.config.aliases['id']]] == selector };
} else if (type == 'object') {
return (doc) => {
if (typeof(doc) != 'object') return false;
for (var m in selector) {
if (this.fields[m]) {
if (typeof(selector[m]) == 'undefined') {
if (doc.hasOwnProperty(this.fields[m])) return false;
} else {
if (lodash.isArray(doc[this.fields[m]])) {
if (lodash.isArray(selector[m])) {
for (var s in selector[m]) {
if (!lodash.includes(doc[this.fields[m]], selector[m][s])) {
return false;
}
}
} else {
if (!lodash.includes(doc[this.fields[m]], selector[m])) {
return false;
}
}
} else {
if (doc[this.fields[m]] != selector[m]) return false;
}
}
}
}
return true;
}
}
}
/**
* Generate adapted for database options object.
*
* @param {Object} [options]
* @return {*} options - a options suitable for the database
*/
options(options) {
var _options = {};
if (options) {
if (options.sort) {
_options.sort = { keys: [], orders: [] };
for (var s in options.sort) {
if (this.fields[s]) {
_options.sort.keys.push(this.fields[s]);
_options.sort.orders.push(options.sort[s]?'asc':'desc');
}
}
}
if (typeof(options.skip) == 'number') {
_options.skip = options.skip;
}
if (typeof(options.limit) == 'number') {
_options.limit = options.limit;
}
}
return _options;
}
/**
* Generate Link from document by fields.
*
* @param {Object} document
* @return {Link} link
*/
_generateLink(document) {
var link = {};
for (var f in this.fields) {
if (document.hasOwnProperty(this.fields[f])) {
link[f] = document[this.fields[f]];
}
}
return link;
}
/**
* Get one first matching link.
*
* @param {string|LinkSelector} selector
* @param {SelectOptions} [options]
* @param {Graph~getCallback} [callback]
* @return {Link} link - result link object
*/
get(selector, options, callback) {
var results = this.fetch(selector, options, (error, results) => {
if (callback) callback(error, results?results[0]:undefined);
});
if (results) return results[0];
}
/**
* Fetch native database documents.
*
* @param {string|linkSelector} selector
* @param {SelectOptions} [options]
* @return {Object[]} documents - result documents
*/
_fetch(selector, options) {
var query = this.query(selector);
var documents = lodash.filter(this.collection, query);
var _options = this.options(options);
if (_options.sort) documents = lodash.orderBy(documents, _options.sort.keys, _options.orders);
var skip = _options.skip?_options.skip:0;
var limit = _options.limit?skip+_options.limit:_options.limit;
documents = documents.slice(skip, limit);
return documents;
}
/**
* Find and all matching links as an Array.
*
* @param {string|LinkSelector} selector
* @param {SelectOptions} [options]
* @param {Graph~fetchCallback} [callback]
* @return {Link[]} links - result links objects in array
*/
fetch(selector, options, callback) {
var documents = this._fetch(selector, options);
var links = [];
for (var d in documents) {
links.push(this._generateLink(documents[d]));
}
if (callback) callback(undefined, links);
return links;
}
/**
* Optional callback. If present, called with an error object as the first argument and, if no error, the result links objects in array.
*
* @callback Graph~fetchCallback
* @param {Error} [error]
* @param {Link[]} [links]
*/
/**
* Should call callback once for each matching document, sequentially and synchronously.
*
* @param {string|LinkSelector} selector
* @param {SelectOptions} [options]
* @param {Graph~eachCallback} [callback]
*/
each(selector, options, callback) {
var links = this.fetch(selector, options);
for (var l in links) {
callback(links[l]);
}
}
/**
* @callback Graph~eachCallback
* @param {Link} [link]
*/
/**
* Map callback over all matching documents. Returns an Array.
*
* @param {string|LinkSelector} selector
* @param {SelectOptions} [options]
* @param {Graph~mapCallback} [callback]
* @return {Array} results
*/
map(selector, options, callback) {
var links = this.fetch(selector, options);
return links.map(callback);
}
/**
* @callback Graph~mapCallback
* @param {Link} [link]
* @return {*} result
*/
/**
* Count all matching documents.
*
* @param {string|LinkSelector} selector
* @param {SelectOptions} [options]
* @param {Graph~countCallback} [callback]
* @return {number} [count]
*/
count(selector, options, callback) {
var links = this.fetch(selector, options);
if (callback) callback(undefined, links.length);
return links.length;
}
/**
* @callback Graph~countCallback
* @param {Error} [error]
* @param {number} [count]
*/
/**
* Should subscribe to the events: link, unlink, insert, update, remove.
*
* @param {string} event - One event name
* @param {Graph~onCallback} callback
* @returns {Function} Stops event subscription.
* @example
* var counter = 0;
* var stop = graph.on('update', (oldData, newData) => {
* if (oldData.id == '1') console.log(oldData.id, 'is changed');
* counter++;
* if (counter == 3) stop();
* });
*/
on(event, callback) {
var subscriptions = [];
if (event == 'insert') {
subscriptions.push(this.emitter.addListener('insert', (document) => {
callback(undefined, this._generateLink(document));
}));
}
if (event == 'update') {
subscriptions.push(this.emitter.addListener('update', (newDocument, oldDocument) => {
callback(this._generateLink(oldDocument), this._generateLink(newDocument));
}));
}
if (event == 'remove') {
subscriptions.push(this.emitter.addListener('remove', (document) => {
callback(this._generateLink(document), undefined);
}));
}
if (event == 'link') {
subscriptions.push(this.emitter.addListener('insert', (document) => {
callback(undefined, this._generateLink(document));
}));
subscriptions.push(this.emitter.addListener('update', (newDocument, oldDocument) => {
callback(this._generateLink(oldDocument), this._generateLink(newDocument));
}));
}
if (event == 'unlink') {
subscriptions.push(this.emitter.addListener('update', (newDocument, oldDocument) => {
callback(this._generateLink(oldDocument), this._generateLink(newDocument));
}));
subscriptions.push(this.emitter.addListener('remove', (document) => {
callback(this._generateLink(document), undefined);
}));
}
return () => {
for (var subscription of subscriptions) {
subscription.remove();
}
};
}
/**
* @callback Graph~onCallback
* @param {Link} [oldLink] - can be undefined on link and insert events
* @param {Link} [newLink] - can be undefined on unlink and remove events
* @param {Object} [context] - additional app information, such as context.userId
*/
}
return ObjectGraph;
};
var ObjectGraph = factoryObjectGraph(AncientGraph);
export { factoryObjectGraph, ObjectGraph as Graph };