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,
- };