neo4jkb
Version:
A graph knowledge base implemented in neo4j.
624 lines (591 loc) • 17.1 kB
JavaScript
// The helper methods to parse data
// dependencies
var _ = require('lomath')
/**
* A conveience method. JSON-stringify the argument, logs it, and return the string.
* @param {JSON} arg The JSON to be stringified.
* @return {string} the stringified JSON.
*/
/* istanbul ignore next */
function log(arg) {
var str = JSON.stringify(arg)
console.log(str)
return str;
}
/**
* Transform and beautify the entire neoRes by internally calling transform(neoRes) -> array of tables -> beautifyMat on each table -> join('\n\n\n'). If neoRes has only one qRes, then returns the single transformed table.
* @param {Array} neoRes Result from Neo4j
* @param {Function|Array} fn A transformer function or many of them in an array
* @param {Boolean} [keepHead=false] To drop the header or not.
* @return {string} The beautified neo4j result
*
* @example
* var neoRes = [{"columns":["a"],"data":[{"row":[{"slack__profile__fields ...
* beautify(neoRes)
* // => '```
* "a"
*
* ---
*
* {
* "slack__profile__fields__Xf0DAVBL83__alt": "",
* ...
* ```'
*
* // if supply a transformer
* function parseUser(userObj) {
* return _.pick(userObj, ['name', 'real_name', 'id', 'email_address'])
* }
*
* beautify(neoRes, parseUser)
* // => '```
* a
*
* name: alice
* id: ID0000001
* email_address: alice@email.com
*
* ---
*
* name: bob
* id: ID0000002
* email_address: bob@email.com
*
* ---
*
* name: slackbot
* real_name: slackbot
* id: USLACKBOT
* email_address: null
* ```'
*
*/
function beautify(neoRes, fn, keepHead) {
// if neoRes has a single qRes table
if (_.size(neoRes) == 1) {
return beautifyMat(transform(neoRes, fn, keepHead))
} else {
return _.map(transform(neoRes, fn), beautifyMat).join('\n\n\n')
}
}
/**
* Beautify for post-transformation: beautify the transformed neoRes by internally calling array of tables -> beautifyMat on each table -> join('\n\n\n'). Note that neoRes must be transformed beforehand. This is for when transformation happens separately. If neoRes has only one qRes, then returns the single transformed table.
* @param {Array} transNeoRes (Transformed) result from Neo4j
* @param {Function|Array} fn A transformer function or many of them in an array
* @return {string} The beautified neo4j result
*
* @example
* var neoRes = [{"columns":["a"],"data":[{"row":[{"slack__profile__fields ...
* var transNeoRes = transform(neoRes, parseUser)
* transBeautify(transNeoRes)
* // => '```
* a
*
* name: alice
* id: ID0000001
* email_address: alice@email.com
*
* ---
*
* name: bob
* id: ID0000002
* email_address: bob@email.com
*
* ---
*
* name: slackbot
* real_name: slackbot
* id: USLACKBOT
* email_address: null
* ```'
*
*/
function transBeautify(transNeoRes) {
if (_.isString(transNeoRes)) {
return beautifyMat([
[transNeoRes]
])
} else if (!_.isArray(_.get(transNeoRes, '0.0'))) {
return beautifyMat(transNeoRes)
} else if (_.size(transNeoRes) == 1) {
return beautifyMat(transNeoRes[0])
} else {
return _.map(transNeoRes, beautifyMat).join('\n\n\n')
}
}
// var neoRes = [{
// "columns": ["a"],
// "data": [{
// "row": [{
// "id": "ID0000001",
// "name": "alice",
// "email_address": "alice@email.com",
// "slack__id": "ID0000001",
// "slack__team_id": "TD0000001",
// "slack__name": "alice",
// "slack__deleted": false,
// "slack__presence": "away",
// "hash_by": "id",
// "hash": "ID0000001",
// "updated_by": "bot",
// "updated_when": "2016-01-29T16:03:19.592Z"
// }, {
// "id": "ID0000002",
// "name": "bob",
// "email_address": "bob@email.com",
// "slack__id": "ID0000002",
// "slack__team_id": "TD0000002",
// "slack__name": "bob",
// "slack__deleted": false,
// "slack__presence": "away",
// "hash_by": "id",
// "hash": "ID0000002",
// "updated_by": "bot",
// "updated_when": "2016-01-29T16:03:19.594Z"
// }, {
// "id": "USLACKBOT",
// "name": "slackbot",
// "real_name": "slackbot",
// "email_address": null,
// "slack__id": "USLACKBOT",
// "slack__team_id": "T07S1438V",
// "slack__name": "slackbot",
// "slack__deleted": false,
// "slack__status": null,
// "slack__color": "757575",
// "slack__real_name": "slackbot",
// "slack__tz": null,
// "slack__tz_label": "Pacific Standard Time",
// "slack__tz_offset": -28800,
// "slack__is_admin": false,
// "slack__is_owner": false,
// "slack__is_primary_owner": false,
// "slack__is_restricted": false,
// "slack__is_ultra_restricted": false,
// "slack__is_bot": false,
// "slack__presence": "active",
// "hash_by": "id",
// "hash": "USLACKBOT",
// "updated_by": "bot",
// "updated_when": "2016-01-29T16:03:19.594Z"
// }]
// }]
// }]
// // console.log(beautify(neoRes, parseUser))
// var transNeoRes = transform(neoRes)
// console.log(transNeoRes)
// transNeoRes = transform(transNeoRes, cleanUser)
// console.log(transNeoRes)
// console.log(transBeautify(transNeoRes))
/**
* Beautify a matrix by JSON.stringify -> join('\n\n') -> join('\n\n---\n\n') to separate rows, and wrap with '```'
* @param {Array} mat Matrix of data to beautify
* @return {string} The beautified matrix string
*
* @example
* var mat = [[{a:1, b:{c:2}}, 0], [{a:3, b:{c:4}}, 1]]
* beautifyMat(mat)
* // =>
* '```
* {
* "a": 1,
* "b": {
* "c": 2
* }
* }
*
* 0
*
* ---
*
* {
* "a": 3,
* "b": {
* "c": 4
* }
* }
*
* 1
* ```'
*
*/
/* istanbul ignore next */
function beautifyMat(mat) {
return _.reduce(mat, function(rsum, row) {
return rsum + _.reduce(row, function(sum, item) {
return (_.isString(sum) ? sum : JSON.stringify(sum, null, 2)) + '\n\n---\n\n' + (_.isString(item) ? item : JSON.stringify(item, null, 2))
}) + '\n'
}, '```\n') + '```'
}
/**
* Format the entire neoRes into an array of qRes tables. Can be called multiply for sequential transformation. If neoRes has only one qRes, then returns the single transformed table.
* @param {Array} neoRes Neo4j raw results, neoRes = [q0res, q1res, ...], or the transformed neoRes.
* @param {Function|Array} fn A transformer function or many of them in an array
* @param {Boolean} [keepHead=false] To drop the header or not.
* @return {Array} neoRes as an array of qRes tables with transformed elements.
*
* @example
* var neoRes = [{"columns":["a"],"data":[{"row":[{"slack__profile__fields ...
* neoRes = transform(neoRes)
* // => [
* [{ slack__profile__fields__Xf0DAVBL83__alt: '',
* ...]
* ]
*
* // if supply a transformer
* function parseUser(userObj) {
* return _.pick(userObj, ['name', 'real_name', 'id', 'email_address'])
* }
*
* // second call in sequence, keep transforming
* // If neoRes has > 1 qRes table
* transform(neoRes, parseUser)
* // => [
* [{ name: 'alice',
* real_name: 'Alice Bloom',
* id: 'ID0000001',
* email_address: 'alice@email.com' },
* ... ]
* ]
*
* // If neoRes has 1 qRes table, return a non-nested result
* transform(neoRes, parseUser)
* // => [{ name: 'alice',
* real_name: 'Alice Bloom',
* id: 'ID0000001',
* email_address: 'alice@email.com' },
* ... ]
*
*
*/
function transform(neoRes, fn, keepHead) {
// singleton, no map
if (_.size(neoRes) == 1) {
return transQRes(neoRes, fn, keepHead)
} else {
return _.map(neoRes, function(qRes) {
return transQRes(qRes, fn, keepHead)
})
}
}
/**
* Format a qRes (query result) inside neoRes into a table. By default, dumps its header.
* @private
* @param {JSON|Array} qRes A {columns, data} pair, or the transformed matrix of {columns, data}
* @param {Function|Array} fn A transformer function or many of them in an array
* @param {Boolean} [keepHead=false] To drop the header or not.
* @return {Array} A table with head and body
*
* @example
* var qRes = {"columns":["a"],"data":[{"row":[{"slack__profile__fields ... }
* transQRes(qRes)
* // => [{ slack__profile__fields__Xf0DAVBL83__alt: '',
* ...]
*
*/
/* istanbul ignore next */
function transQRes(qRes, fn, keepHead) {
// check if qRes is not already a matrix (transformed)
qRes = _.isPlainObject(qRes[0]) ? qRes[0] : qRes
keepHead = _.isUndefined(keepHead) ? false : keepHead;
var headExist = true;
var head, body;
if (qRes.columns) {
head = qRes.columns
head[0] = 'header: ' + head[0].replace('header: ', '')
body = transData(qRes.data, fn)
} else {
// check if header is still there
if (!_.startsWith(_.get(qRes, '0.0'), 'header: ')) {
headExist = false;
};
head = headExist ? _.head(qRes) : []
body = headExist ? transData(_.tail(qRes), fn) : transData(qRes, fn);
}
// if wishes to keep the head if it exists
if (headExist && keepHead) {
body.unshift(head)
};
return body
}
/**
* Format the data array of row objects into plain matrix.
* @private
* @param {Array} data of row objects, or the transformed data.
* @param {Function|Array} fn A transformer function or many of them in an array
* @return {Array} Matrix of data
*
* @example
* var data = [{"row":[{"slack__profile__fields ...
* transData(data)
* // => [ [ { slack__profile__fields__Xf0DAVBL83__alt: '', ...}
* [ { ... }]
* ...
* ]
*
* // if supply a transformer
* function parseUser(userObj) {
* return _.pick(userObj, ['name', 'real_name', 'id', 'email_address'])
* }
*
* transData(data, parseUser)
* // => [
* [ { name: 'alice',
* real_name: 'Alice Bloom',
* id: 'ID0000001',
* email_address: 'alice@email.com' },
* ... ]
* ]
*/
/* istanbul ignore next */
function transData(data, fn) {
fn = fn || _.identity
if (_.isArray(fn)) {
return _.map(data, function(obj) {
var row = _.isPlainObject(obj) ? obj.row : obj
return _.map(row, function(item) {
// return fn(item)
return _.reduce(fn, function(sum, f) {
return f(sum)
}, item)
})
})
} else {
return _.map(data, function(obj) {
var row = _.isPlainObject(obj) ? obj.row : obj
return _.map(row, fn)
})
}
}
///////////////////////////
// built-in transformers //
///////////////////////////
/**
* Parse a JSON object into array to ['k: v', 'k: v'], where v is attemptedly stringified.
* @param {JSON} obj Object to parse
* @return {Array} of strings like ['k: v', 'k: v']
*
* var obj = {
* a: 1,
* b: {c:2}
* }
* parseKV(obj)
* // => 'a: 1\nb: {\n "c": 2\n}'
*
*/
function parseKV(obj) {
return _.map(obj, function(v, k) {
v = _.isString(v) ? v : JSON.stringify(v, null, 2)
return k + ': ' + v
}).join('\n')
}
/**
* Cleanup the user object by picking out name, real_name, id, email_address.
* @param {JSON} userObj The user node property object
* @return {JSON} The cleaned prop object
*
* @example
*
* var user = {
* "id": "ID0000001",
* "name": "alice",
* "email_address": "alice@email.com",
* "slack": {
* "id": "ID0000001",
* "team_id": "TD0000001",
* "name": "alice",
* "deleted": false,
* "presence": "away"
* }
* }
*
* cleanUser(user)
* // => {
* // "id": "ID0000001",
* // "name": "alice",
* // "email_address": "alice@email.com",
* // }
*/
function cleanUser(userObj) {
return _.pick(userObj, ['name', 'real_name', 'id', 'email_address'])
}
/**
* A beautify transformer method to parse user, picking out name, real_name, id, email_address; uses parseKV internally.
* @param {JSON} userObj The user node property object
* @return {string} The parsed string of user.
*
* @example
*
* var user = {
* "id": "ID0000001",
* "name": "alice",
* "email_address": "alice@email.com",
* "slack": {
* "id": "ID0000001",
* "team_id": "TD0000001",
* "name": "alice",
* "deleted": false,
* "presence": "away"
* }
* }
* parseUser(user)
* // => 'name: alice
* // id: ID0000001
* // email_address: alice@email.com'
*/
function parseUser(userObj) {
return parseKV(cleanUser(userObj))
}
/**
* A beautify transformer method to parse object, picking out keys from keyArr; uses parseKV internally.
* @param {JSON} obj The object
* @param {Array} keyArr Of key to pick
* @return {Array} The parsed string of object.
*/
function parseObj(obj, keyArr) {
return parseKV(_.pick(obj, keyArr))
}
////////////////////////////////
// Higher level query helpers //
////////////////////////////////
// helper: generates a WHERE tail string to match multiple properties to a single match
// var ws = 'WHERE' + leftJoin(['a.name', 'a.real_name', 'a.id', 'a.email_address'], `=~ "(?i).*${keyword}.*"`)
/**
* Helper to generate wOp for matching multiple properties to the same value.
* @param {Array} propArr Array of strings of prop, may be prepended with the subjects 'a, e, b' or not (defaulted to a.)
* @param {string} match The match operator string.
* @param {string} [boolOp='OR'] The boolean to concat these matches together.
* @return {string} wOp string.
*
* @example
* var ws = 'WHERE ' + leftJoin(['name', 'real_name', 'a.id', 'a.email_address'], '=~ "(?i).*alice.*"')
* // => WHERE a.name=~ "(?i).*alice.*" OR a.real_name=~ "(?i).*alice.*" OR a.id=~ "(?i).*alice.*" OR a.email_address=~ "(?i).*alice.*"
* // note that 'name' and 'real_name' and defaulted to 'a.name' and 'a.real_name'
*
* // changing the default operator to AND
* var ws = 'WHERE' + leftJoin(['name', 'real_name', 'a.id', 'a.email_address'], '=~ "(?i).*alice.*"', 'AND')
* // => WHERE a.name=~ "(?i).*alice.*" AND a.real_name=~ "(?i).*alice.*" AND a.id=~ "(?i).*alice.*" AND a.email_address=~ "(?i).*alice.*"
*
*/
function leftJoin(propArr, match, boolOp) {
if (!match) {
throw new Error('Must specify match string, e.g. =~ "(?i).*keyword.*" ')
};
boolOp = boolOp || 'OR'
var subjected = _.map(propArr, function(pStr) {
return _.includes(pStr, '.') ? pStr : 'a.' + pStr
})
return ' ' + _.map(subjected, function(pStr) {
return pStr + match
}).join(' ' + boolOp + ' ')
}
/**
* Generate a function to sort the rows of a matrix passed to it using _.sortBy and iteratees.
* @param {Function|Object|string|Array} iteratees Of _.sortBy
* @return {Function} To sort rows in, and flatten a taken matrix.
*/
function sorter(iteratees) {
iteratees = iteratees || ['name']
return _.partial(_sorter, _, iteratees)
}
// helper for sorter
/* istanbul ignore next*/
function _sorter(mat, iteratees) {
var size = _.size(mat)
return _.chunk(_.sortBy(_.flatten(mat), iteratees), size)
}
/**
* For use with transform. Generate a picker function using _.pick with a supplied iteratees.
* @param {string|Array} iteratees Of _.pick
* @return {Function} That picks iteratees of its argument.
*/
function picker(iteratees) {
iteratees = iteratees || ['name']
return _.partial(_.pick, _, iteratees)
}
/**
* For use with transform. Generate a picker function using _.pickBy with a supplied iteratees.
* @param {string|Array} iteratees Of _.pickBy
* @return {Function} That picks by iteratees of its argument.
*/
function pickerBy(iteratees) {
return _.partial(_.pickBy, _, iteratees)
}
/**
* Flatten and index a matrix of objects into formatted string. 'name' key will always appear first next to index.
* @param {Array} mat Matrix of JSON objects or string.
* @return {string} The formatted string
*
* @example
* flattenIndex([
* [{
* name: 'alice',
* id: 'ID0000001',
* email_address: 'alice@email.com'
* }],
* [{
* name: 'bob',
* id: 'ID0000002',
* email_address: 'bob@email.com'
* }],
* [{
* name: 'slackbot',
* real_name: 'slackbot',
* id: 'USLACKBOT',
* email_address: null
* }]
* ])
* // => 0. alice
* id: ID0000001
* email_address: alice@email.com
* 1. bob
* id: ID0000002
* email_address: bob@email.com
* 2. slackbot
* real_name: slackbot
* id: USLACKBOT
* email_address: null
*
* flattenIndex([
* [{
* name: 'alice'
* }],
* [{
* name: 'bob'
* }],
* [{
* name: 'slackbot'
* }]
* ])
* // => 0. alice
* 1. bob
* 2. slackbot
*/
function flattenIndex(mat) {
var flat = _.flattenDeep(mat)
return _.map(flat, function(o, i) {
// check if o is string, plain concat
return _.isString(o) ?
i + '. ' + o :
i + '. ' + (o['name'] || '') +
_.map(_.omit(o, 'name'), function(v, k) {
return '\n' + k + ': ' + v
}).join('')
}).join('\n')
}
module.exports = {
log: log,
beautify: beautify,
transBeautify: transBeautify,
beautifyMat: beautifyMat,
transform: transform,
parseKV: parseKV,
cleanUser: cleanUser,
parseUser: parseUser,
parseObj: parseObj,
leftJoin: leftJoin,
sorter: sorter,
picker: picker,
pickerBy: pickerBy,
flattenIndex: flattenIndex
}