app/modules/entities/crud/mapper.js
const _ = require('lodash');
const queries = require('./query');
const aggs = require('./aggregation');
const Search = require('./search');
const errors = require('../../exceptions/errors');
const SortOrder = require('./enums/sort_order');
const SortMode = require('./enums/sort_mode');
function get_sort(sorts) {
if (!sorts || sorts.length === 0) {
return [];
}
return sorts.map((sort) => {
if (_.isString(sort)) {
const fc = sort[0];
if (fc === '-') {
return { [sort.slice(1)]: 'desc' };
}
return { [sort]: 'asc' };
}
return sort;
});
}
class Mapper {
static check_wellform_object(obj) {
const keys = Object.keys(obj);
const contains_dollars = keys.some(key => key.startsWith('$$'));
const contains_dollar = keys.some(key => key.startsWith('$') && !key.startsWith('$$'));
const contains_shortcut = keys.every(key => !key.startsWith('$'));
return [contains_dollar, contains_dollars, contains_shortcut];
}
static auto_nest(types) {
return types.reduce((acc, elt) => {
const [key, parent, type] = elt;
switch (type) {
case 'nested': {
if (acc.length === 0) {
const nq = new queries.Nested();
acc = [nq, nq];
} else if (acc[0] instanceof queries.Nested) {
const nq = new queries.Nested();
acc[1].query(nq);
acc[0] = nq;
}
if (parent === '') {
acc[0].path(key);
} else {
acc[0].path(`${parent}.${key}`);
}
return acc;
}
default:
return acc;
}
}, []);
}
static raw_query(obj) {
const keys = Object.keys(obj);
if (keys[0].startsWith('$$')) {
return new queries.RawQuery({ [keys[0].replace('$$', '')]: obj[keys[0]] });
}
return null;
}
static make_ids_query(value) {
if (value instanceof Array) {
return new queries.Ids({ values: value });
}
return new queries.Ids({ values: [value] });
}
static shortcut_query(obj, mapping) {
const keys = Object.keys(obj);
if (keys.length === 0) {
return null;
}
const key = keys[0];
if (key === '_id') {
return Mapper.make_ids_query(obj[key]);
}
const types = mapping.get_all_type(key);
if (types.length === 0) {
return null;
}
const infos = types[types.length - 1];
const type = infos[infos.length - 1];
const value = obj[key];
let outer_query = null;
let most_inner_query = null;
if (types.length > 1) {
[most_inner_query, outer_query] = Mapper.auto_nest(types);
}
if (value instanceof Array) {
switch (type) {
case 'text': {
const matches = value.map((elt) => {
if (elt instanceof Object) {
return Mapper.special_shortcut_query(key, type, elt);
} else if (elt.startsWith('"') && elt.endsWith('"')) {
return new queries.MatchPhrase().match({ [key]: elt.slice(1, -1) });
}
return new queries.Match().match({ [key]: elt });
}).filter(elt => elt != null);
const bool = matches.reduce((q, elt) => q.should(elt), new queries.Bool());
if (outer_query != null) {
most_inner_query.query(bool);
return outer_query;
}
return bool;
}
case 'date': {
const range = value.reduce((q, elt) => q.operators(elt), new queries.Range().field(key));
if (outer_query != null) {
most_inner_query.query(range);
return outer_query;
}
return range;
}
default: {
const terms = new queries.Terms({ [key]: value });
if (outer_query != null) {
most_inner_query.query(terms);
return outer_query;
}
return terms;
}
}
} else if (value instanceof Object) {
const q = Mapper.special_shortcut_query(key, type, value);
if (q != null && outer_query != null) {
most_inner_query.query(q);
return outer_query;
}
if (q == null && type === 'date') {
const range = new queries.Range().field(key).operators(value);
if (outer_query != null) {
most_inner_query.query(range);
return outer_query;
}
return range;
}
return q;
} else {
switch (type) {
case 'text': {
let q = null;
if (value.startsWith('"') && value.endsWith('"')) {
q = new queries.MatchPhrase().match({ [key]: value.slice(1, -1) });
} else {
q = new queries.Match().match({ [key]: value });
}
if (outer_query != null) {
most_inner_query.query(q);
return outer_query;
}
return q;
}
default: {
const q = new queries.Term({ [key]: value });
if (outer_query != null) {
most_inner_query.query(q);
return outer_query;
}
return q;
}
}
}
}
static special_shortcut_query(key, type, object) {
if ('$qs' in object) {
return new queries.QueryString().qs(object.$qs).default_field(key);
}
if ('$match' in object && 'query' in object.$match) {
return new queries.Match(object.$match).match({ [key]: object.$match.query });
}
return null;
}
static visit_object(obj, mapping) {
const result = Mapper.check_wellform_object(obj);
if (result.filter(r => r === true) >= 2) {
throw errors.InvalidObject();
}
const [contains_dollar, contains_dollars, contains_shortcut] = result;
if (contains_dollar) {
return Mapper.bool_query(obj, mapping);
} else if (contains_dollars) {
return Mapper.raw_query(obj);
} else if (contains_shortcut) {
return Mapper.shortcut_query(obj, mapping);
}
return null;
}
static visit_bool_query(bool, obj, op, mapping) {
const result = Mapper.visit_object(obj, mapping);
switch (op) {
default:
case '$and':
if (result) {
bool.must(result);
}
break;
case '$fand':
if (result) {
bool.filter(result);
}
break;
case '$nfand':
if (result) {
bool.must_not(result);
}
break;
case '$or':
if (result) {
bool.should(result);
}
break;
}
return bool;
}
static visit_list(bool, list, op, mapping) {
list.forEach((obj) => {
bool = Mapper.visit_bool_query(bool, obj, op, mapping);
});
return bool;
}
static bool_query(obj, mapping) {
let bool = new queries.Bool();
['$and', '$fand', '$nfand', '$or', '$msm', '$minimum_should_match'].forEach((op) => {
if (!(op in obj)) {
return;
}
const val = obj[op];
if (val instanceof Array) {
bool = Mapper.visit_list(bool, val, op, mapping);
} else if (op === '$msm' || op === '$minimum_should_match') {
bool.minimum_should_match(val);
} else {
bool = Mapper.visit_bool_query(bool, val, op, mapping);
}
});
return bool;
}
}
class SortMapper {
static visit_object(sort, mapping) {
const sorts = sort.map(s => SortMapper.make_single_sort(s, mapping))
.filter(s => s != null);
return sorts;
}
static make_single_sort(sort, mapping) {
if (typeof sort === 'string') {
sort = { [sort]: [] };
}
const final_sort_obj = {};
const keys = Object.keys(sort);
if (keys.length === 0) {
return null;
}
const key = keys[0];
// We don't analyze scripted sort
if (key === '_script') {
final_sort_obj[key] = sort[key];
return final_sort_obj;
}
const types = mapping.get_all_type(key);
const nested_fields = types.filter(elt => elt[elt.length - 1] === 'nested');
if (nested_fields.length > 1) {
return null;
}
final_sort_obj[key] = {};
if (nested_fields.length === 1) {
final_sort_obj[key].nested_path = nested_fields[0][0];
}
const value = sort[key];
if (value instanceof Array) {
const tmp = SortMapper.retrieve_mode_and_order(value);
final_sort_obj[key] = _.merge(final_sort_obj[key], tmp);
} else {
const tmp = SortMapper.retrieve_mode_and_order([value]);
final_sort_obj[key] = _.merge(final_sort_obj[key], tmp);
}
return final_sort_obj;
}
static retrieve_mode_and_order(array) {
const final_obj = array.reduce((acc, elt) => {
const oen = SortOrder.enumValueOf(elt.toUpperCase());
if (oen !== undefined) {
acc.order = oen.toString();
}
const men = SortMode.enumValueOf(elt.toUpperCase());
if (men !== undefined) {
acc.mode = men.toString();
}
return acc;
}, {});
return final_obj;
}
}
class AggregationMapper {
static auto_nest(types) {
return types.reduce((acc, elt) => {
const [key, parent, type] = elt;
switch (type) {
case 'nested': {
if (acc.length === 0) {
const nq = new aggs.NestedAggregation(`${key}_nested`);
acc = [nq, nq];
} else if (acc[0] instanceof aggs.NestedAggregation) {
const nq = new aggs.NestedAggregation(`${key}_nested`);
acc[1].aggregation(nq);
acc[0] = nq;
}
if (parent === '') {
acc[0].path(key);
} else {
acc[0].path(`${parent}.${key}`);
}
return acc;
}
default:
return acc;
}
}, []);
}
static check_aggregation_with_required_field(agg_type) {
switch (agg_type) {
case 'top_hits':
return false; // Does not require a field to work
default:
return true;
}
}
static visit_object(aggregations, mapping) {
const a = _.reduce(aggregations, (obj, value, key) => {
const agg = AggregationMapper.visit_single_aggregation(key, value, mapping);
if (agg != null) {
obj = _.merge(obj, agg.generate());
}
return obj;
}, {});
return a;
}
static visit_single_aggregation(field, aggregation, mapping) {
if (!('$type' in aggregation)) {
return null;
}
const type = aggregation.$type;
const name = aggregation.$name || `${field}_${type}`;
delete aggregation.$type;
if ('$name' in aggregation) {
delete aggregation.$name;
}
let most_outer_agg = null;
if ('$filter' in aggregation) {
most_outer_agg = new aggs.FilterAggregation(`${field}_filter`).query(aggregation.$filter);
}
const types = mapping.get_all_type(field);
if (types.length === 0 && AggregationMapper.check_aggregation_with_required_field(type)) {
return null;
}
let most_inner_agg = null;
let outer_agg = null;
if (types.length > 1) {
[most_inner_agg, outer_agg] = AggregationMapper.auto_nest(types);
}
let agg = AggregationMapper.forge_aggregation(name, field, type,
aggregation, mapping);
if (most_inner_agg != null && agg != null) {
most_inner_agg.aggregation(agg);
}
if ('$aggregations' in aggregation) {
const subaggregations = aggregation.$aggregations;
if (agg instanceof aggs.BucketAggregation) {
agg = _.reduce(subaggregations,
AggregationMapper.visit_subaggregation.bind(null, mapping), agg);
}
}
if (most_outer_agg != null) {
if (outer_agg != null) {
most_outer_agg.aggregation(outer_agg);
} else {
most_outer_agg.aggregation(agg);
}
return most_outer_agg;
} else if (most_inner_agg != null) {
return outer_agg;
}
return agg;
}
static visit_subaggregation(mapping, obj, subvalue, subfield) {
const subagg = AggregationMapper
.visit_single_aggregation(subfield, subvalue, mapping);
if (subagg != null) {
obj.aggregation(subagg);
}
return obj;
}
static forge_aggregation(name, field, type, aggregation, mapping) {
switch (type) {
case 'min':
return new aggs.MinAggregation(name, aggregation).field(field);
case 'avg':
return new aggs.AvgAggregation(name, aggregation).field(field);
case 'max':
return new aggs.MaxAggregation(name, aggregation).field(field);
case 'sum':
return new aggs.SumAggregation(name, aggregation).field(field);
case 'value_count':
return new aggs.ValueCountAggregation(name, aggregation).field(field);
case 'cardinality':
return new aggs.CardinalityAggregation(name, aggregation).field(field);
case 'terms':
return new aggs.TermsAggregation(name, aggregation).field(field);
case 'date_histogram':
return new aggs.DateHistogramAggregation(name, aggregation).field(field);
case 'top_hits':
if ('sort' in aggregation) {
const transformed_sort = SortMapper.visit_object(get_sort(aggregation.sort), mapping);
aggregation.sort = transformed_sort;
}
return new aggs.TopHitsAggregation(name, aggregation);
case 'filter': {
if (!('$query' in aggregation)) {
return null;
}
/* let result = Mapper.visit_object(aggregation.$query, mapping);
if (result == null) {
result = new queries.MatchAll();
}*/
return new aggs.FilterAggregation(name, aggregation).query(aggregation.$query);
}
default:
return null;
}
}
}
function transform_to_search(body, mapping) {
const s = new Search();
if (!('where' in body)) {
s.query(new queries.MatchAll());
return s;
}
const where = body.where;
const result = Mapper.visit_object(where, mapping);
if (result) {
s.query(result);
} else {
s.query(new queries.MatchAll());
}
return s;
}
function transform_to_sort(body, mapping) {
if (!('sort' in body)) {
return null;
}
const sort = body.sort;
const sort_transformed = get_sort(sort);
const result = SortMapper.visit_object(sort_transformed, mapping);
return result;
}
function transform_to_aggregation(body, mapping) {
if (!('aggregations' in body)) {
return null;
}
const agg = body.aggregations;
const result = AggregationMapper.visit_object(agg, mapping);
return result;
}
module.exports = {
transform_to_search,
transform_to_sort,
transform_to_aggregation,
};