I totally suck at recursion. I have a function that works but it also looks horribly wrong. I would love to 1) Know how to write it properly 2) Know about resources to read/study so that I don't get so stuck again.
The function is simple: given something like this:
queryConditions = {
type: 'and',
args: [
// First of all: video must be published or belonging to logged in user
{ type: 'or', args: [
{ type: 'eq', args: [ 'published', true ] },
{ type: 'eq', args: [ 'userId', '#_loggedInUser#' ] },
]},
// Second: filter by userId if passed
{ type: 'eq', args: [ 'userId', '#userId#'] },
{ type: 'or', args: [
{ type: 'contains', args: [ 'title', '#s#' ] },
{ type: 'contains', args: [ 'videosTags.tagName', '#s#' ] },
] }
]
},
It will return a copy of it, but:
- If
ifDefined
is set and the equivalent attribute inconditionsHash
isn't set, the entry is skipped - If a "leaf" has, as second field, something in this
#format#
, then its content is exchanged withconditionsHash['format']
. - if
or
orand
only have one argument, then they are eliminated and replaced by their argument That means that you can't have{ type: 'or', args: [ { type: 'eq', args: [ 'published', true ] } ]}
since there is only one argument inor
,
I am especially uneasy about the fact that I check twice if the type is 'and' or 'or'. I need to fully check this because I need to add a feature to this: I need to add the ability to add an each
parameter:
// Third: must satisfy _each_ condition of #s#, space-separated
{ type: 'each', 'value': 's', type: 'and', separator: ' ', args: [
{ type: 'or', args: [
{ type: 'contains', args: [ 'title', '#s#' ] },
{ type: 'contains', args: [ 'videosTags.tagName', '#s#' ] },
] }
] }
Which creates an entry for every space-separated string in conditionsHash['s']
. That way, I can make composite queries if needed. At this point, I don't quite understand where to plug that in.
Here is the function...
function getQueryFromQueryConditions( initialConditions, conditionsHash, allowedFields ){
// Make up filter.conditions
function visitQueryConditions( o, fc ){
// Check o.ifDefined. If the corresponding element in conditionsHash
// is not defined, won't go there
if( o.ifDefined ){
if( ! conditionsHash[ o.ifDefined ] ){ return false; }
}
if( o.type === 'and' || o.type === 'or'){
fc.type = o.type;
fc.args = [];
o.args.forEach( function( condition ){
// If it's 'and' or 'or', check the length of what gets returned
if( condition.type === 'and' || condition.type === 'or' ){
// Make up the new condition, visit that one
var newCondition = {};
var f = visitQueryConditions( condition, newCondition );
// Falsy return means "don't continue"
if( f === false ) return;
// newCondition is empty: do not add anything to fc
if( newCondition.args.length === 0 ){
return ;
// Only one condition returned: get rid of logical operator, add the straight condition
} else if( newCondition.args.length === 1 ){
var actualCondition = newCondition.args[ 0 ];
fc.args.push( { type: actualCondition.type, args: actualCondition.args } );
// Multiple conditions returned: the logical operator makes sense
} else {
fc.args.push( newCondition );
}
// If it's a leaf
} else {
var newCondition = {};
var f = visitQueryConditions( condition, newCondition );
if( f !== false ) fc.args.push( newCondition );
}
});
// It's a terminal point
} else {
var arg0 = o.args[ 0 ];
var arg1 = o.args[ 1 ];
// No arg1: most likely a unary operator, let it live.
if( typeof( arg1 ) === 'undefined'){
fc.type = o.type;
fc.args = [];
fc.args[ 0 ] = arg0;
}
// The second argument has a "but!".
// If it is in form #something#, it means that it's
// actually a field in conditionsHash
var m = ( arg1.match && arg1.match( /^#(.*?)#$/) );
if( m ) {
var osf = m[ 1 ];
// If it's in form #something#, then entry MUST be in allowedFields
if( ! allowedFields[ osf ] ) throw new Error("Searched for " + arg1 + ", but didn't find corresponding entry in onlineSearchSchema");
if( conditionsHash[ osf ] ){
fc.type = o.type;
fc.args = [];
fc.args[ 0 ] = arg0;
fc.args[ 1 ] = conditionsHash[ osf ];
} else {
// For leaves, this will tell the callee NOT to add this.
return false;
};
// The second argument is not in form #something#: it means it's a STRAIGHT value
} else {
fc.type = o.type;
fc.args = [];
fc.args[ 0 ] = arg0;
fc.args[ 1 ] = arg1;
}
}
}
var res = {};
visitQueryConditions( initialConditions, res );
// visitQueryConditions does a great job avoiding duplication, but
// top-level duplication needs to be checked here
if( ( res.type === 'and' || res.type === 'or' )){
if( res.args.length === 0 ) return {};
if( res.args.length === 1 ) return res.args[ 0 ];
}
return res;
}
var initialConditions = {
type: 'and',
args: [
// First of all: video must be published or belonging to logged in user
{ type: 'or', args: [
{ type: 'eq', args: [ 'published', true ] },
{ type: 'eq', args: [ 'userId', '#_loggedInUser#' ] },
]},
// Second: filter by userId if passed
{ type: 'eq', args: [ 'userId', '#userId#'] },
{ type: 'or', args: [
{ type: 'contains', args: [ 'title', '#s#' ] },
{ type: 'contains', args: [ 'videosTags.tagName', '#s#' ] },
] }
]
};
var conditionsHash = {
s: "some",
userId: "44",
_loggedInUser: "55"
};
var allowedFields = {
_loggedInUser: true,
userId: true,
s: true
}
var conditions = getQueryFromQueryConditions( initialConditions, conditionsHash, allowedFields );
console.log( require('util').inspect( conditions, { depth: 10 }) );
It can run on straight node and it will work (I adapted it from the existing code)