Manual Reference Source

app/modules/entities/crud/mapper.js

  1. const _ = require('lodash');
  2. const queries = require('./query');
  3. const aggs = require('./aggregation');
  4. const Search = require('./search');
  5. const errors = require('../../exceptions/errors');
  6. const SortOrder = require('./enums/sort_order');
  7. const SortMode = require('./enums/sort_mode');
  8.  
  9. function get_sort(sorts) {
  10. if (!sorts || sorts.length === 0) {
  11. return [];
  12. }
  13.  
  14. return sorts.map((sort) => {
  15. if (_.isString(sort)) {
  16. const fc = sort[0];
  17. if (fc === '-') {
  18. return { [sort.slice(1)]: 'desc' };
  19. }
  20. return { [sort]: 'asc' };
  21. }
  22. return sort;
  23. });
  24. }
  25.  
  26. class Mapper {
  27. static check_wellform_object(obj) {
  28. const keys = Object.keys(obj);
  29. const contains_dollars = keys.some(key => key.startsWith('$$'));
  30. const contains_dollar = keys.some(key => key.startsWith('$') && !key.startsWith('$$'));
  31. const contains_shortcut = keys.every(key => !key.startsWith('$'));
  32. return [contains_dollar, contains_dollars, contains_shortcut];
  33. }
  34.  
  35. static auto_nest(types) {
  36. return types.reduce((acc, elt) => {
  37. const [key, parent, type] = elt;
  38. switch (type) {
  39. case 'nested': {
  40. if (acc.length === 0) {
  41. const nq = new queries.Nested();
  42. acc = [nq, nq];
  43. } else if (acc[0] instanceof queries.Nested) {
  44. const nq = new queries.Nested();
  45. acc[1].query(nq);
  46. acc[0] = nq;
  47. }
  48.  
  49. if (parent === '') {
  50. acc[0].path(key);
  51. } else {
  52. acc[0].path(`${parent}.${key}`);
  53. }
  54. return acc;
  55. }
  56. default:
  57. return acc;
  58. }
  59. }, []);
  60. }
  61.  
  62. static raw_query(obj) {
  63. const keys = Object.keys(obj);
  64. if (keys[0].startsWith('$$')) {
  65. return new queries.RawQuery({ [keys[0].replace('$$', '')]: obj[keys[0]] });
  66. }
  67. return null;
  68. }
  69.  
  70. static make_ids_query(value) {
  71. if (value instanceof Array) {
  72. return new queries.Ids({ values: value });
  73. }
  74. return new queries.Ids({ values: [value] });
  75. }
  76.  
  77. static shortcut_query(obj, mapping) {
  78. const keys = Object.keys(obj);
  79. if (keys.length === 0) {
  80. return null;
  81. }
  82.  
  83. const key = keys[0];
  84.  
  85. if (key === '_id') {
  86. return Mapper.make_ids_query(obj[key]);
  87. }
  88.  
  89. const types = mapping.get_all_type(key);
  90. if (types.length === 0) {
  91. return null;
  92. }
  93.  
  94. const infos = types[types.length - 1];
  95. const type = infos[infos.length - 1];
  96. const value = obj[key];
  97. let outer_query = null;
  98. let most_inner_query = null;
  99. if (types.length > 1) {
  100. [most_inner_query, outer_query] = Mapper.auto_nest(types);
  101. }
  102.  
  103. if (value instanceof Array) {
  104. switch (type) {
  105. case 'text': {
  106. const matches = value.map((elt) => {
  107. if (elt instanceof Object) {
  108. return Mapper.special_shortcut_query(key, type, elt);
  109. } else if (elt.startsWith('"') && elt.endsWith('"')) {
  110. return new queries.MatchPhrase().match({ [key]: elt.slice(1, -1) });
  111. }
  112. return new queries.Match().match({ [key]: elt });
  113. }).filter(elt => elt != null);
  114. const bool = matches.reduce((q, elt) => q.should(elt), new queries.Bool());
  115.  
  116. if (outer_query != null) {
  117. most_inner_query.query(bool);
  118. return outer_query;
  119. }
  120. return bool;
  121. }
  122. case 'date': {
  123. const range = value.reduce((q, elt) => q.operators(elt), new queries.Range().field(key));
  124. if (outer_query != null) {
  125. most_inner_query.query(range);
  126. return outer_query;
  127. }
  128. return range;
  129. }
  130. default: {
  131. const terms = new queries.Terms({ [key]: value });
  132. if (outer_query != null) {
  133. most_inner_query.query(terms);
  134. return outer_query;
  135. }
  136. return terms;
  137. }
  138. }
  139. } else if (value instanceof Object) {
  140. const q = Mapper.special_shortcut_query(key, type, value);
  141. if (q != null && outer_query != null) {
  142. most_inner_query.query(q);
  143. return outer_query;
  144. }
  145.  
  146. if (q == null && type === 'date') {
  147. const range = new queries.Range().field(key).operators(value);
  148. if (outer_query != null) {
  149. most_inner_query.query(range);
  150. return outer_query;
  151. }
  152. return range;
  153. }
  154. return q;
  155. } else {
  156. switch (type) {
  157. case 'text': {
  158. let q = null;
  159. if (value.startsWith('"') && value.endsWith('"')) {
  160. q = new queries.MatchPhrase().match({ [key]: value.slice(1, -1) });
  161. } else {
  162. q = new queries.Match().match({ [key]: value });
  163. }
  164. if (outer_query != null) {
  165. most_inner_query.query(q);
  166. return outer_query;
  167. }
  168. return q;
  169. }
  170. default: {
  171. const q = new queries.Term({ [key]: value });
  172. if (outer_query != null) {
  173. most_inner_query.query(q);
  174. return outer_query;
  175. }
  176. return q;
  177. }
  178. }
  179. }
  180. }
  181.  
  182. static special_shortcut_query(key, type, object) {
  183. if ('$qs' in object) {
  184. return new queries.QueryString().qs(object.$qs).default_field(key);
  185. }
  186.  
  187. if ('$match' in object && 'query' in object.$match) {
  188. return new queries.Match(object.$match).match({ [key]: object.$match.query });
  189. }
  190. return null;
  191. }
  192.  
  193. static visit_object(obj, mapping) {
  194. const result = Mapper.check_wellform_object(obj);
  195.  
  196. if (result.filter(r => r === true) >= 2) {
  197. throw errors.InvalidObject();
  198. }
  199.  
  200. const [contains_dollar, contains_dollars, contains_shortcut] = result;
  201. if (contains_dollar) {
  202. return Mapper.bool_query(obj, mapping);
  203. } else if (contains_dollars) {
  204. return Mapper.raw_query(obj);
  205. } else if (contains_shortcut) {
  206. return Mapper.shortcut_query(obj, mapping);
  207. }
  208. return null;
  209. }
  210.  
  211. static visit_bool_query(bool, obj, op, mapping) {
  212. const result = Mapper.visit_object(obj, mapping);
  213.  
  214. switch (op) {
  215. default:
  216. case '$and':
  217. if (result) {
  218. bool.must(result);
  219. }
  220. break;
  221. case '$fand':
  222. if (result) {
  223. bool.filter(result);
  224. }
  225. break;
  226. case '$nfand':
  227. if (result) {
  228. bool.must_not(result);
  229. }
  230. break;
  231. case '$or':
  232. if (result) {
  233. bool.should(result);
  234. }
  235. break;
  236. }
  237. return bool;
  238. }
  239.  
  240. static visit_list(bool, list, op, mapping) {
  241. list.forEach((obj) => {
  242. bool = Mapper.visit_bool_query(bool, obj, op, mapping);
  243. });
  244. return bool;
  245. }
  246.  
  247.  
  248. static bool_query(obj, mapping) {
  249. let bool = new queries.Bool();
  250. ['$and', '$fand', '$nfand', '$or', '$msm', '$minimum_should_match'].forEach((op) => {
  251. if (!(op in obj)) {
  252. return;
  253. }
  254.  
  255. const val = obj[op];
  256. if (val instanceof Array) {
  257. bool = Mapper.visit_list(bool, val, op, mapping);
  258. } else if (op === '$msm' || op === '$minimum_should_match') {
  259. bool.minimum_should_match(val);
  260. } else {
  261. bool = Mapper.visit_bool_query(bool, val, op, mapping);
  262. }
  263. });
  264. return bool;
  265. }
  266. }
  267.  
  268. class SortMapper {
  269. static visit_object(sort, mapping) {
  270. const sorts = sort.map(s => SortMapper.make_single_sort(s, mapping))
  271. .filter(s => s != null);
  272. return sorts;
  273. }
  274.  
  275. static make_single_sort(sort, mapping) {
  276. if (typeof sort === 'string') {
  277. sort = { [sort]: [] };
  278. }
  279.  
  280. const final_sort_obj = {};
  281. const keys = Object.keys(sort);
  282. if (keys.length === 0) {
  283. return null;
  284. }
  285.  
  286. const key = keys[0];
  287.  
  288. // We don't analyze scripted sort
  289. if (key === '_script') {
  290. final_sort_obj[key] = sort[key];
  291. return final_sort_obj;
  292. }
  293.  
  294. const types = mapping.get_all_type(key);
  295.  
  296. const nested_fields = types.filter(elt => elt[elt.length - 1] === 'nested');
  297. if (nested_fields.length > 1) {
  298. return null;
  299. }
  300.  
  301. final_sort_obj[key] = {};
  302.  
  303. if (nested_fields.length === 1) {
  304. final_sort_obj[key].nested_path = nested_fields[0][0];
  305. }
  306.  
  307. const value = sort[key];
  308. if (value instanceof Array) {
  309. const tmp = SortMapper.retrieve_mode_and_order(value);
  310. final_sort_obj[key] = _.merge(final_sort_obj[key], tmp);
  311. } else {
  312. const tmp = SortMapper.retrieve_mode_and_order([value]);
  313. final_sort_obj[key] = _.merge(final_sort_obj[key], tmp);
  314. }
  315.  
  316. return final_sort_obj;
  317. }
  318.  
  319. static retrieve_mode_and_order(array) {
  320. const final_obj = array.reduce((acc, elt) => {
  321. const oen = SortOrder.enumValueOf(elt.toUpperCase());
  322. if (oen !== undefined) {
  323. acc.order = oen.toString();
  324. }
  325. const men = SortMode.enumValueOf(elt.toUpperCase());
  326. if (men !== undefined) {
  327. acc.mode = men.toString();
  328. }
  329. return acc;
  330. }, {});
  331. return final_obj;
  332. }
  333. }
  334.  
  335. class AggregationMapper {
  336. static auto_nest(types) {
  337. return types.reduce((acc, elt) => {
  338. const [key, parent, type] = elt;
  339. switch (type) {
  340. case 'nested': {
  341. if (acc.length === 0) {
  342. const nq = new aggs.NestedAggregation(`${key}_nested`);
  343. acc = [nq, nq];
  344. } else if (acc[0] instanceof aggs.NestedAggregation) {
  345. const nq = new aggs.NestedAggregation(`${key}_nested`);
  346. acc[1].aggregation(nq);
  347. acc[0] = nq;
  348. }
  349.  
  350. if (parent === '') {
  351. acc[0].path(key);
  352. } else {
  353. acc[0].path(`${parent}.${key}`);
  354. }
  355. return acc;
  356. }
  357. default:
  358. return acc;
  359. }
  360. }, []);
  361. }
  362.  
  363. static check_aggregation_with_required_field(agg_type) {
  364. switch (agg_type) {
  365. case 'top_hits':
  366. return false; // Does not require a field to work
  367. default:
  368. return true;
  369. }
  370. }
  371.  
  372.  
  373. static visit_object(aggregations, mapping) {
  374. const a = _.reduce(aggregations, (obj, value, key) => {
  375. const agg = AggregationMapper.visit_single_aggregation(key, value, mapping);
  376. if (agg != null) {
  377. obj = _.merge(obj, agg.generate());
  378. }
  379. return obj;
  380. }, {});
  381. return a;
  382. }
  383.  
  384. static visit_single_aggregation(field, aggregation, mapping) {
  385. if (!('$type' in aggregation)) {
  386. return null;
  387. }
  388.  
  389. const type = aggregation.$type;
  390. const name = aggregation.$name || `${field}_${type}`;
  391.  
  392. delete aggregation.$type;
  393. if ('$name' in aggregation) {
  394. delete aggregation.$name;
  395. }
  396.  
  397. let most_outer_agg = null;
  398. if ('$filter' in aggregation) {
  399. most_outer_agg = new aggs.FilterAggregation(`${field}_filter`).query(aggregation.$filter);
  400. }
  401.  
  402. const types = mapping.get_all_type(field);
  403. if (types.length === 0 && AggregationMapper.check_aggregation_with_required_field(type)) {
  404. return null;
  405. }
  406.  
  407.  
  408. let most_inner_agg = null;
  409. let outer_agg = null;
  410. if (types.length > 1) {
  411. [most_inner_agg, outer_agg] = AggregationMapper.auto_nest(types);
  412. }
  413.  
  414. let agg = AggregationMapper.forge_aggregation(name, field, type,
  415. aggregation, mapping);
  416. if (most_inner_agg != null && agg != null) {
  417. most_inner_agg.aggregation(agg);
  418. }
  419.  
  420. if ('$aggregations' in aggregation) {
  421. const subaggregations = aggregation.$aggregations;
  422. if (agg instanceof aggs.BucketAggregation) {
  423. agg = _.reduce(subaggregations,
  424. AggregationMapper.visit_subaggregation.bind(null, mapping), agg);
  425. }
  426. }
  427.  
  428. if (most_outer_agg != null) {
  429. if (outer_agg != null) {
  430. most_outer_agg.aggregation(outer_agg);
  431. } else {
  432. most_outer_agg.aggregation(agg);
  433. }
  434. return most_outer_agg;
  435. } else if (most_inner_agg != null) {
  436. return outer_agg;
  437. }
  438. return agg;
  439. }
  440.  
  441. static visit_subaggregation(mapping, obj, subvalue, subfield) {
  442. const subagg = AggregationMapper
  443. .visit_single_aggregation(subfield, subvalue, mapping);
  444. if (subagg != null) {
  445. obj.aggregation(subagg);
  446. }
  447. return obj;
  448. }
  449.  
  450.  
  451. static forge_aggregation(name, field, type, aggregation, mapping) {
  452. switch (type) {
  453. case 'min':
  454. return new aggs.MinAggregation(name, aggregation).field(field);
  455. case 'avg':
  456. return new aggs.AvgAggregation(name, aggregation).field(field);
  457. case 'max':
  458. return new aggs.MaxAggregation(name, aggregation).field(field);
  459. case 'sum':
  460. return new aggs.SumAggregation(name, aggregation).field(field);
  461. case 'value_count':
  462. return new aggs.ValueCountAggregation(name, aggregation).field(field);
  463. case 'cardinality':
  464. return new aggs.CardinalityAggregation(name, aggregation).field(field);
  465. case 'terms':
  466. return new aggs.TermsAggregation(name, aggregation).field(field);
  467. case 'date_histogram':
  468. return new aggs.DateHistogramAggregation(name, aggregation).field(field);
  469. case 'top_hits':
  470. if ('sort' in aggregation) {
  471. const transformed_sort = SortMapper.visit_object(get_sort(aggregation.sort), mapping);
  472. aggregation.sort = transformed_sort;
  473. }
  474. return new aggs.TopHitsAggregation(name, aggregation);
  475. case 'filter': {
  476. if (!('$query' in aggregation)) {
  477. return null;
  478. }
  479.  
  480. /* let result = Mapper.visit_object(aggregation.$query, mapping);
  481. if (result == null) {
  482. result = new queries.MatchAll();
  483. }*/
  484. return new aggs.FilterAggregation(name, aggregation).query(aggregation.$query);
  485. }
  486. default:
  487. return null;
  488. }
  489. }
  490. }
  491.  
  492. function transform_to_search(body, mapping) {
  493. const s = new Search();
  494. if (!('where' in body)) {
  495. s.query(new queries.MatchAll());
  496. return s;
  497. }
  498.  
  499. const where = body.where;
  500. const result = Mapper.visit_object(where, mapping);
  501.  
  502. if (result) {
  503. s.query(result);
  504. } else {
  505. s.query(new queries.MatchAll());
  506. }
  507. return s;
  508. }
  509.  
  510. function transform_to_sort(body, mapping) {
  511. if (!('sort' in body)) {
  512. return null;
  513. }
  514.  
  515. const sort = body.sort;
  516. const sort_transformed = get_sort(sort);
  517. const result = SortMapper.visit_object(sort_transformed, mapping);
  518. return result;
  519. }
  520.  
  521. function transform_to_aggregation(body, mapping) {
  522. if (!('aggregations' in body)) {
  523. return null;
  524. }
  525.  
  526. const agg = body.aggregations;
  527. const result = AggregationMapper.visit_object(agg, mapping);
  528. return result;
  529. }
  530.  
  531.  
  532. module.exports = {
  533. transform_to_search,
  534. transform_to_sort,
  535. transform_to_aggregation,
  536. };