PredicateCore.js

  1. /**
  2. * Rules
  3. * @module core
  4. * @namespace core
  5. * @since 1.0.0
  6. * @note rules are 100% tested from PredicateCore.test.js
  7. */
  8. const {
  9. merge,
  10. find,
  11. curry,
  12. prop,
  13. tap,
  14. pipe,
  15. filter,
  16. map,
  17. over,
  18. lens,
  19. lensPath,
  20. takeLast,
  21. set,
  22. differenceWith,
  23. lensProp,
  24. insert,
  25. } = require('ramda');
  26. const option = require('option');
  27. function head(list) {
  28. return option.fromNullable(list[0]).value();
  29. }
  30. module.exports = function({ dataclasses, invariants, errors, rules }) {
  31. const {
  32. CompoundPredicate,
  33. ComparisonPredicate,
  34. Predicate,
  35. Target,
  36. LogicalType,
  37. } = dataclasses;
  38. /**
  39. * Get a type by its type_id
  40. * @param {array} types
  41. * @param {string} type_id type id name
  42. * @return {?Type} a Type
  43. * @private
  44. * @since 1.0.0
  45. */
  46. const _getTypeById = (types, type_id) =>
  47. option.fromNullable(find(type => type.type_id == type_id, types));
  48. /**
  49. * Get a target by its target_id
  50. * @param {array} targets
  51. * @param {string} target_id target id name
  52. * @return {?dataclasses.Target}
  53. * @private
  54. * @since 1.0.0
  55. */
  56. const _getTargetById = (targets, target_id) =>
  57. option.fromNullable(find(target => target.target_id == target_id, targets));
  58. /**
  59. * Get a logical type by its logicalType_id
  60. * @param {array} logicalTypes
  61. * @param {string} logicalType_id logicalType id name
  62. * @return {?dataclasses.Target}
  63. * @private
  64. * @since 1.0.0
  65. */
  66. const _getLogicalTypeById = (logicalTypes, logicalType_id) =>
  67. option.fromNullable(
  68. find(
  69. logicalType => logicalType.logicalType_id == logicalType_id,
  70. logicalTypes
  71. )
  72. );
  73. /**
  74. * _getOperatorsByIds
  75. * @param {Object} columns
  76. * @param {string[]} operator_ids
  77. * @return {Array<dataclasses.operator>}
  78. * @private
  79. * @since 1.0.0
  80. */
  81. const _getOperatorsByIds = curry((operators, operator_ids) =>
  82. pipe(filter(({ operator_id }) => operator_ids.includes(operator_id)))(
  83. operators
  84. )
  85. );
  86. const _set$operatorsToType = curry((columns, type) => {
  87. type.$operators = _getOperatorsByIds(columns.operators, type.operator_ids);
  88. return type;
  89. });
  90. const _set$typeToTarget = curry((columns, target) => {
  91. const typeOption = _getTypeById(columns.types, target.type_id);
  92. return invariants
  93. .TargetMustReferToADefinedType(typeOption, target)
  94. .then(type => {
  95. target.$type = type;
  96. return target;
  97. });
  98. });
  99. /**
  100. * Tap for Promise
  101. * @param {Function} f
  102. * @return {Function}
  103. * @private
  104. */
  105. const _tapPromise = f => {
  106. return function(promise) {
  107. return promise.then(result => {
  108. f();
  109. return result;
  110. });
  111. };
  112. };
  113. /**
  114. * Run `fAfter()` (without any arguments) after `fBefore`, it will yield the promise yield from fBefore
  115. * @param {Function} fBefore
  116. * @param {Function} fAfter
  117. * @return {Promise} promise from fBefore
  118. * @private
  119. */
  120. const _afterPromise = (fBefore, fAfter) => pipe(fBefore, _tapPromise(fAfter));
  121. // columns => Promise[columns]
  122. const initializeColumns = columns => {
  123. // at first I used lenses, but the code was way harder to read so it's better that way :)
  124. // wrap operators
  125. columns.operators = map(dataclasses.Operator, columns.operators);
  126. // wrap logicalTypes
  127. columns.logicalTypes = map(dataclasses.LogicalType, columns.logicalTypes);
  128. // wrap types and set `$operators` attribute on each type
  129. const wrapType = pipe(dataclasses.Type, _set$operatorsToType(columns));
  130. columns.types = map(wrapType, columns.types);
  131. // wrap targets and set `$type` attribut on each target
  132. const wrapTarget = pipe(dataclasses.Target, _set$typeToTarget(columns));
  133. return Promise.all(map(wrapTarget, columns.targets)).then(targets => {
  134. columns.targets = targets;
  135. return columns;
  136. });
  137. };
  138. /**
  139. * Create a new PredicateCore
  140. * @param {?dataclasses.CompoundPredicate} [data=PredicateCore.defaults.options.getDefaultData]
  141. * @param {Object} [columns=PredicateCore.defaults.columns]
  142. * @param {Object} [options=PredicateCore.defaults.options]
  143. * @return {Promise<core.PredicateCoreAPI>}
  144. * @memberof core
  145. */
  146. function PredicateCore({ data, columns, options } = {}) {
  147. return initializeColumns(columns || PredicateCore.defaults.columns).then(
  148. _columns => {
  149. let _root;
  150. const _options = merge(PredicateCore.defaults.options, options);
  151. /**
  152. * Loop through the predicate tree and update flags (e.g. $canBeRemoved)
  153. * @private
  154. */
  155. function _apply$flags() {
  156. const canRemoveAnyPredicate = !rules.predicateToRemoveIsTheLastComparisonPredicate(
  157. _root,
  158. CompoundPredicate,
  159. ComparisonPredicate
  160. );
  161. CompoundPredicate.forEach(_root, function(predicate) {
  162. predicate.$canBeRemoved =
  163. canRemoveAnyPredicate &&
  164. !rules.predicateToRemoveIsRootPredicate(_root, predicate);
  165. });
  166. }
  167. /**
  168. * Set PredicateCore data
  169. * @param {dataclasses.CompoundPredicate} root CompoundPredicate
  170. * @return {Promise<undefined, errors.RootPredicateMustBeACompoundPredicate>} resolved promise yield nothing, rejected promise yield RootPredicateMustBeACompoundPredicate error
  171. * @since 1.0.0
  172. * @memberof core.api
  173. */
  174. function setData(root) {
  175. return invariants
  176. .RootPredicateMustBeACompoundPredicate(root, CompoundPredicate)
  177. .then(() => {
  178. _root = root;
  179. });
  180. }
  181. /**
  182. * Add a ComparisonPredicate or CompoundPredicate
  183. * @param {Object} option
  184. * @param {string} options.type what type of Predicate to add
  185. * @param {string} [options.how=after] should we insert it before, after or instead of? (currently only after is supported)
  186. * @param {dataclasses.Predicate} options.where current element
  187. * @return {Promise<dataclasses.Predicate>} inserted predicate
  188. * @since 1.0.0
  189. * @memberof core.api
  190. */
  191. function add({ where, how = 'after', type }) {
  192. // currently only after is supported
  193. return (
  194. Promise.resolve()
  195. .then(() => invariants.AddOnlySupportsAfter(how))
  196. .then(() =>
  197. invariants.PredicateTypeMustBeValid(type, Predicate.Types)
  198. )
  199. // generate the Predicates
  200. .then(() => _options[`getDefault${type}`](_columns, _options))
  201. // then add it
  202. .then(predicate => {
  203. const isComparisonPredicate = ComparisonPredicate.is(where);
  204. if (isComparisonPredicate || CompoundPredicate.is(where)) {
  205. if (isComparisonPredicate) {
  206. // it's a comparisonpredicate
  207. // first find predicates array that contains the element
  208. const path = _find(where);
  209. // we are starting from a ComparisonPredicate that always live inside a CompoundPredicate.predicates array
  210. const [compoundpredicate, [_, index]] = takeLast(2, path);
  211. compoundpredicate.predicates = insert(
  212. index + 1,
  213. predicate,
  214. compoundpredicate.predicates
  215. );
  216. } else {
  217. // it's a compoundpredicate
  218. // we want to add a CompoundPredicate after a compound predicate
  219. // so we need to add it as its first .predicates entry
  220. where.predicates.unshift(predicate);
  221. }
  222. return predicate;
  223. }
  224. return Promise.reject(
  225. new errors.CannotAddSomethingElseThanACompoundPredicateOrAComparisonPredicate()
  226. );
  227. })
  228. );
  229. }
  230. /**
  231. * Remove a ComparisonPredicate or CompoundPredicate
  232. * @param {(dataclasses.ComparisonPredicate|dataclasses.CompoundPredicate)} predicate
  233. * @return {Promise<dataclasses.Predicate>} yield the removed predicate, will reject the promise if remove was called with the root CompoundPredicate or the last ComparisonPredicate of the root CompoundPredicate
  234. * @since 1.0.0
  235. * @memberof core.api
  236. */
  237. function remove(predicate) {
  238. return Promise.resolve()
  239. .then(() =>
  240. invariants.RemovePredicateMustDifferFromRootPredicate(
  241. _root,
  242. predicate
  243. )
  244. )
  245. .then(() =>
  246. invariants.RemovePredicateCannotBeTheLastComparisonPredicate(
  247. _root,
  248. predicate,
  249. CompoundPredicate,
  250. ComparisonPredicate
  251. )
  252. )
  253. .then(() => {
  254. if (
  255. CompoundPredicate.is(predicate) ||
  256. ComparisonPredicate.is(predicate)
  257. ) {
  258. const path = _find(predicate);
  259. // we are starting from a ComparisonPredicate that always live
  260. // inside a CompoundPredicate.predicates array
  261. const [parentCompoundpredicate, [_, index]] = takeLast(2, path);
  262. parentCompoundpredicate.predicates.splice(index, 1);
  263. if (parentCompoundpredicate.predicates.length === 0) {
  264. // if there are not any more predicates
  265. // inside the parentCompoundpredicate, we should also remove it
  266. return remove(parentCompoundpredicate);
  267. }
  268. return predicate;
  269. }
  270. return Promise.reject(
  271. new errors.CannotRemoveSomethingElseThanACompoundPredicateOrAComparisonPredicate()
  272. );
  273. });
  274. }
  275. /**
  276. * Change a CompoundPredicate logical
  277. * @param {dataclasses.CompoundPredicate} predicate
  278. * @param {string} newLogicalType_id
  279. * @return {Promise<undefined, errors.PredicateMustBeACompoundPredicate>} yield nothing if everything went right, otherwise yield a reject promise with the PredicateMustBeACompoundPredicate error
  280. * @since 1.0.0
  281. * @memberof core.api
  282. */
  283. function setPredicateLogicalType_id(predicate, newLogicalType_id) {
  284. return invariants
  285. .PredicateMustBeACompoundPredicate(predicate, CompoundPredicate)
  286. .then(() => {
  287. // first change the logical type
  288. return _getLogicalTypeById(
  289. _columns.logicalTypes,
  290. newLogicalType_id
  291. );
  292. })
  293. .then(logicalTypeOption =>
  294. invariants.LogicalType_idMustReferToADefinedLogicalType(
  295. logicalTypeOption
  296. )
  297. )
  298. .then(logicalTypeOption => {
  299. predicate.logic = logicalTypeOption.value(); // safe
  300. });
  301. }
  302. /**
  303. * Change a predicate's target
  304. * @param {dataclasses.ComparisonPredicate} predicate
  305. * @param {string} newTarget_id
  306. * @return {Promise} yield nothing if everything went right, otherwise yield a reject promise with the PredicateMustBeAComparisonPredicate error
  307. * @since 1.0.0
  308. * @memberof core.api
  309. */
  310. function setPredicateTarget_id(predicate, newTarget_id) {
  311. return invariants
  312. .PredicateMustBeAComparisonPredicate(predicate, ComparisonPredicate)
  313. .then(() => {
  314. // first change the target
  315. return _getTargetById(_columns.targets, newTarget_id);
  316. })
  317. .then(targetOption =>
  318. invariants.Target_idMustReferToADefinedTarget(targetOption)
  319. )
  320. .then(targetOption => {
  321. predicate.target = targetOption.value(); // safe
  322. // then change the operator to the first operator for this target
  323. return setPredicateOperator_id(
  324. predicate,
  325. head(predicate.target.$type.$operators).operator_id
  326. );
  327. });
  328. }
  329. /**
  330. * Change a predicate's operator
  331. * @param {dataclasses.ComparisonPredicate} predicate
  332. * @param {string} newTarget_id
  333. * @return {Promise<undefined, errors.PredicateMustBeAComparisonPredicate>} yield nothing if everything went right, otherwise yield a reject promise with the PredicateMustBeAComparisonPredicate error
  334. * @since 1.0.0
  335. * @memberof core.api
  336. */
  337. function setPredicateOperator_id(predicate, newOperator_id) {
  338. return (
  339. Promise.resolve()
  340. // find operator
  341. .then(() =>
  342. option.fromNullable(
  343. predicate.target.$type.$operators.find(
  344. operator => operator.operator_id === newOperator_id
  345. )
  346. )
  347. )
  348. .then(operatorOption =>
  349. invariants.Operator_idMustReferToADefinedOperator(
  350. operatorOption
  351. )
  352. )
  353. // change the operator
  354. .then(operatorOption => {
  355. predicate.operator = operatorOption.value(); // safe
  356. // then reset arguments to array
  357. predicate.arguments = [];
  358. })
  359. );
  360. }
  361. /**
  362. * Compute the JSON pointer path the element
  363. * @param {Object} element (http://jsonpatch.com/)
  364. * @return {?Array} null if not found
  365. * @readonly
  366. * @since 1.0.0
  367. */
  368. function _find(element) {
  369. return CompoundPredicate.reduce(
  370. _root,
  371. (acc, predicate, parents) => {
  372. return element === predicate ? parents : acc;
  373. },
  374. null
  375. );
  376. }
  377. // get data for initialization
  378. return (
  379. (data
  380. ? Promise.resolve(data)
  381. : _options.getDefaultData(_columns, _options)
  382. )
  383. // setup PredicateCore data
  384. .then(_afterPromise(setData, _apply$flags))
  385. // expose public API
  386. .then(() => {
  387. /**
  388. * ui-predicate core public API
  389. * @typedef {object} PredicateCoreAPI
  390. * @namespace core.api
  391. */
  392. return {
  393. setData: _afterPromise(setData, _apply$flags),
  394. add: _afterPromise(add, _apply$flags),
  395. remove: _afterPromise(remove, _apply$flags),
  396. setPredicateTarget_id: _afterPromise(
  397. setPredicateTarget_id,
  398. _apply$flags
  399. ),
  400. setPredicateOperator_id: _afterPromise(
  401. setPredicateOperator_id,
  402. _apply$flags
  403. ),
  404. setPredicateLogicalType_id: _afterPromise(
  405. setPredicateLogicalType_id,
  406. _apply$flags
  407. ),
  408. /**
  409. * Get root CompoundPredicate
  410. * @return {dataclasses.CompoundPredicate}
  411. * @memberof core.api
  412. */
  413. get root() {
  414. return _root;
  415. },
  416. toJSON() {
  417. return _root;
  418. },
  419. // used for testing
  420. get columns() {
  421. return _columns;
  422. },
  423. // used for testing
  424. get options() {
  425. return _options;
  426. },
  427. };
  428. })
  429. );
  430. }
  431. );
  432. }
  433. /**
  434. * Defaults configuration of PredicateCore
  435. * @type {Object}
  436. * @namespace core.defaults
  437. */
  438. PredicateCore.defaults = {
  439. options: {
  440. /**
  441. * When data is not set at construction time PredicateCore default behavior will be to use the first target and its first operator with empty argument
  442. * @param {Object} dataclasses every necessary data class
  443. * @param {Object} columns every necessary data class
  444. * @param {Object} options PredicateCore available options
  445. * @return {Promise<dataclasses.CompoundPredicate>} root CompoundPredicate
  446. * @since 1.0.0
  447. * @memberof core.defaults
  448. */
  449. getDefaultData(columns, options) {
  450. return options
  451. .getDefaultComparisonPredicate(columns, options)
  452. .then(comparisonPredicate => {
  453. return options.getDefaultCompoundPredicate(columns, options, [
  454. comparisonPredicate,
  455. ]);
  456. });
  457. },
  458. /**
  459. * Default compount predicate to use
  460. *
  461. * This function is called whenever a new CompoundPredicate is added to the UIPredicate
  462. * @param {Array<dataclasses.Predicate>} predicates
  463. * @param {Object} columns specified columns
  464. * @param {Object} options PredicateCore available options
  465. * @return {Promise<dataclasses.CompoundPredicate>} a CompoundPredicate
  466. * @since 1.0.0
  467. * @memberof core.defaults
  468. */
  469. getDefaultCompoundPredicate(columns, options, predicates) {
  470. return (!Array.isArray(predicates) || predicates.length === 0
  471. ? options
  472. .getDefaultComparisonPredicate(columns, options)
  473. .then(comparisonPredicate => [comparisonPredicate])
  474. : Promise.resolve(predicates)
  475. ).then(predicates =>
  476. options
  477. .getDefaultLogicalType(predicates, columns, options)
  478. .then(logicalType => CompoundPredicate(logicalType, predicates))
  479. );
  480. },
  481. /**
  482. * Default comparison predicate to use
  483. *
  484. * This function is called whenever a new ComparisonPredicate is added to the UIPredicate
  485. * @param {Object} columns specified columns
  486. * @param {Object} [options=PredicateCore.defaults.options] PredicateCore available options
  487. * @return {Promise<dataclasses.ComparisonPredicate>} a Comparison
  488. * @since 1.0.0
  489. * @memberof core.defaults
  490. */
  491. getDefaultComparisonPredicate(columns, options) {
  492. const firstTarget = head(columns.targets);
  493. return ComparisonPredicate(
  494. firstTarget,
  495. head(firstTarget.$type.$operators),
  496. []
  497. );
  498. },
  499. /**
  500. * Default logical type to use when a new comparison predicate is created
  501. *
  502. * This function is called whenever a new ComparisonPredicate is added to the UIPredicate
  503. * @param {Array<dataclasses.Predicate>} predicates specified columns
  504. * @param {Object} columns specified columns
  505. * @param {Object} [options=PredicateCore.defaults.options] PredicateCore available options
  506. * @return {Promise<dataclasses.LogicalType>} a logical type
  507. * @since 1.0.0
  508. * @memberof core.defaults
  509. */
  510. getDefaultLogicalType(predicates, columns, options) {
  511. return Promise.resolve(head(columns.logicalTypes));
  512. },
  513. },
  514. columns: {
  515. // besides array list names, everything else follows convention https://github.com/FGRibreau/sql-convention
  516. operators: [
  517. {
  518. operator_id: 'is',
  519. label: 'Est',
  520. },
  521. {
  522. operator_id: 'contains',
  523. label: 'Contient',
  524. },
  525. {
  526. operator_id: 'isLowerThan',
  527. label: '<',
  528. },
  529. {
  530. operator_id: 'isEqualTo',
  531. label: '=',
  532. },
  533. {
  534. operator_id: 'isHigherThan',
  535. label: '>',
  536. },
  537. {
  538. operator_id: 'isBetween',
  539. label: 'est compris entre',
  540. },
  541. ],
  542. types: [
  543. {
  544. type_id: 'int',
  545. operator_ids: ['isLowerThan', 'isEqualTo', 'isHigherThan'],
  546. },
  547. {
  548. type_id: 'string',
  549. operator_ids: ['is', 'contains'],
  550. },
  551. {
  552. type_id: 'datetime',
  553. operator_ids: ['is', 'isBetween'],
  554. },
  555. ],
  556. targets: [
  557. {
  558. target_id: 'article.title',
  559. label: 'Titre article',
  560. type_id: 'string',
  561. },
  562. {
  563. target_id: 'videoCount',
  564. label: 'Nombre de vidéo',
  565. type_id: 'int',
  566. },
  567. {
  568. target_id: 'publishingAt',
  569. label: 'Date de publication',
  570. type_id: 'datetime',
  571. },
  572. ],
  573. logicalTypes: [
  574. {
  575. logicalType_id: 'any',
  576. label: 'Any',
  577. },
  578. {
  579. logicalType_id: 'all',
  580. label: 'All',
  581. },
  582. {
  583. logicalType_id: 'none',
  584. label: 'None',
  585. },
  586. ],
  587. },
  588. };
  589. return PredicateCore;
  590. };