property-list.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. "use strict";
  2. var _ = require('../../lodash'), PropertyBase = require('./property-base').PropertyBase, __PARENT = '__parent', DEFAULT_INDEX_ATTR = 'id', DEFAULT_INDEXCASE_ATTR = false, DEFAULT_INDEXMULTI_ATTR = false, PropertyList;
  3. /**
  4. * An item constructed of PropertyList.Type.
  5. * @typedef {Object} PropertyList.Type
  6. */
  7. _.inherit((
  8. /**
  9. * @constructor
  10. * @param {Function} type
  11. * @param {Object} parent
  12. * @param {Array} populate
  13. */
  14. PropertyList = function PostmanPropertyList(type, parent, populate) {
  15. // @todo add this test sometime later
  16. // if (!type) {
  17. // throw new Error('postman-collection: cannot initialise a list without a type parameter');
  18. // }
  19. PropertyList.super_.call(this); // call super with appropriate options
  20. this.setParent(parent); // save reference to parent
  21. _.assign(this, /** @lends PropertyList.prototype */ {
  22. /**
  23. * @private
  24. * @type {Array}
  25. */
  26. members: this.members || [],
  27. /**
  28. * @private
  29. * @type {Object}
  30. * @note This should not be used, and it's not guaranteed to be in sync with the actual list of members.
  31. */
  32. reference: this.reference || {},
  33. /**
  34. * @private
  35. * @type {Function}
  36. */
  37. Type: type
  38. });
  39. // if the type this list holds has its own index key, then use the same
  40. _.getOwn(type, '_postman_propertyIndexKey') && (this._postman_listIndexKey = type._postman_propertyIndexKey);
  41. // if the type has case sensitivity flags, set the same
  42. _.getOwn(type, '_postman_propertyIndexCaseInsensitive') && (this._postman_listIndexCaseInsensitive =
  43. type._postman_propertyIndexCaseInsensitive);
  44. // if the type allows multiple values, set the flag
  45. _.getOwn(type, '_postman_propertyAllowsMultipleValues') && (this._postman_listAllowsMultipleValues =
  46. type._postman_propertyAllowsMultipleValues);
  47. // prepopulate
  48. populate && this.populate(populate);
  49. }), PropertyBase);
  50. _.assign(PropertyList.prototype, /** @lends PropertyList.prototype */ {
  51. /**
  52. * Indicates that this element contains a number of other elements.
  53. * @private
  54. */
  55. _postman_propertyIsList: true,
  56. /**
  57. * Holds the attribute to index this PropertyList by. Default: 'id'
  58. *
  59. * @private
  60. * @type {String}
  61. */
  62. _postman_listIndexKey: DEFAULT_INDEX_ATTR,
  63. /**
  64. * Holds the attribute whether indexing of this list is case sensitive or not
  65. *
  66. * @private
  67. * @type {String}
  68. */
  69. _postman_listIndexCaseInsensitive: DEFAULT_INDEXCASE_ATTR,
  70. /**
  71. * Holds the attribute whether exporting the index retains duplicate index items
  72. *
  73. * @private
  74. * @type {String}
  75. */
  76. _postman_listAllowsMultipleValues: DEFAULT_INDEXMULTI_ATTR,
  77. /**
  78. * Insert an element at the end of this list. When a reference member specified via second parameter is found, the
  79. * member is inserted at an index before the reference member.
  80. *
  81. * @param {PropertyList.Type} item
  82. * @param {PropertyList.Type|String} [before]
  83. */
  84. insert: function (item, before) {
  85. if (!_.isObject(item)) {
  86. return;
  87. } // do not proceed on empty param
  88. var duplicate = this.indexOf(item), index;
  89. // remove from previous list
  90. PropertyList.isPropertyList(item[__PARENT]) && (item[__PARENT] !== this) && item[__PARENT].remove(item);
  91. // inject the parent reference
  92. _.assignHidden(item, __PARENT, this);
  93. // ensure that we do not double insert things into member array
  94. (duplicate > -1) && this.members.splice(duplicate, 1);
  95. // find the position of the reference element
  96. before && (before = this.indexOf(before));
  97. // inject to the members array ata position or at the end in case no item is there for reference
  98. (before > -1) ? this.members.splice(before, 0, item) : this.members.push(item);
  99. // store reference by id, so create the index string. we first ensure that the index value is truthy and then
  100. // recheck that the string conversion of the same is truthy as well.
  101. if ((index = item[this._postman_listIndexKey]) && (index = String(index))) {
  102. // desensitise case, if the property needs it to be
  103. this._postman_listIndexCaseInsensitive && (index = index.toLowerCase());
  104. // if multiple values are allowed, the reference may contain an array of items, mapped to an index.
  105. if (this._postman_listAllowsMultipleValues && Object.hasOwnProperty.call(this.reference, index)) {
  106. // if the value is not an array, convert it to an array.
  107. !_.isArray(this.reference[index]) && (this.reference[index] = [this.reference[index]]);
  108. // add the item to the array of items corresponding to this index
  109. this.reference[index].push(item);
  110. }
  111. else {
  112. this.reference[index] = item;
  113. }
  114. }
  115. },
  116. /**
  117. * Insert an element at the end of this list. When a reference member specified via second parameter is found, the
  118. * member is inserted at an index after the reference member.
  119. *
  120. * @param {PropertyList.Type} item
  121. * @param {PropertyList.Type|String} [after]
  122. */
  123. insertAfter: function (item, after) {
  124. // convert item to positional reference
  125. return this.insert(item, this.idx(this.indexOf(after) + 1));
  126. },
  127. /**
  128. * Adds or moves an item to the end of this list.
  129. *
  130. * @param {PropertyList.Type} item
  131. */
  132. append: function (item) {
  133. return this.insert(item);
  134. },
  135. /**
  136. * Adds or moves an item to the beginning of this list.
  137. *
  138. * @param {PropertyList.Type} item
  139. */
  140. prepend: function (item) {
  141. return this.insert(item, this.idx(0));
  142. },
  143. /**
  144. * Add an item or item definition to this list.
  145. *
  146. * @param {Object|PropertyList.Type} item
  147. * @todo
  148. * - remove item from original parent if already it has a parent
  149. * - validate that the original parent's constructor matches this parent's constructor
  150. */
  151. add: function (item) {
  152. // do not proceed on empty param, but empty strings are in fact valid.
  153. // eslint-disable-next-line lodash/prefer-is-nil
  154. if (_.isNull(item) || _.isUndefined(item) || _.isNaN(item)) {
  155. return;
  156. }
  157. // create new instance of the item based on the type specified if it is not already
  158. this.insert((item.constructor === this.Type) ? item :
  159. // if the property has a create static function, use it.
  160. // eslint-disable-next-line prefer-spread
  161. (_.has(this.Type, 'create') ? this.Type.create.apply(this.Type, arguments) : new this.Type(item)));
  162. },
  163. /**
  164. * Add an item or update an existing item
  165. *
  166. * @param {PropertyList.Type} item
  167. *
  168. * @returns {?Boolean}
  169. */
  170. upsert: function (item) {
  171. // do not proceed on empty param, but empty strings are in fact valid.
  172. if (_.isNil(item) || _.isNaN(item)) {
  173. return null;
  174. }
  175. var indexer = this._postman_listIndexKey, existing = this.one(item[indexer]);
  176. if (existing) {
  177. if (!_.isFunction(existing.update)) {
  178. throw new Error('collection: unable to upsert into a list of Type that does not support .update()');
  179. }
  180. existing.update(item);
  181. return false;
  182. }
  183. // since there is no existing item, just add a new one
  184. this.add(item);
  185. return true; // indicate added
  186. },
  187. /**
  188. * Removes all elements from the PropertyList for which the predicate returns truthy.
  189. *
  190. * @param {Function|String|PropertyList.Type} predicate
  191. * @param {Object} context Optional context to bind the predicate to.
  192. */
  193. remove: function (predicate, context) {
  194. var match; // to be used if predicate is an ID
  195. !context && (context = this);
  196. if (_.isString(predicate)) {
  197. // if predicate is id, then create a function to remove that
  198. // need to take care of case sensitivity as well :/
  199. match = this._postman_listIndexCaseInsensitive ? predicate.toLowerCase() : predicate;
  200. predicate = function (item) {
  201. var id = item[this._postman_listIndexKey];
  202. this._postman_listIndexCaseInsensitive && (id = id.toLowerCase());
  203. return id === match;
  204. }.bind(this);
  205. }
  206. else if (predicate instanceof this.Type) {
  207. // in case an object reference is sent, prepare it for removal using direct reference comparison
  208. match = predicate;
  209. predicate = function (item) {
  210. return (item === match);
  211. };
  212. }
  213. _.isFunction(predicate) && _.remove(this.members, function (item) {
  214. var index;
  215. if (predicate.apply(context, arguments)) {
  216. if ((index = item[this._postman_listIndexKey]) && (index = String(index))) {
  217. this._postman_listIndexCaseInsensitive && (index = index.toLowerCase());
  218. if (this._postman_listAllowsMultipleValues && _.isArray(this.reference[index])) {
  219. // since we have an array of multiple values, remove only the value for which the
  220. // predicate returned truthy. If the array becomes empty, just delete it.
  221. _.remove(this.reference[index], function (each) {
  222. return each === item;
  223. });
  224. // If the array becomes empty, remove it
  225. (this.reference[index].length === 0) && (delete this.reference[index]);
  226. // If the array contains only one element, remove the array, and assign the element
  227. // as the reference value
  228. (this.reference[index].length === 1) && (this.reference[index] = this.reference[index][0]);
  229. }
  230. else {
  231. delete this.reference[index];
  232. }
  233. }
  234. delete item[__PARENT]; // unlink from its parent
  235. return true;
  236. }
  237. }.bind(this));
  238. },
  239. /**
  240. * Removes all items in the list
  241. */
  242. clear: function () {
  243. // we unlink every member from it's parent (assuming this is their parent)
  244. this.all().forEach(PropertyList._unlinkItemFromParent);
  245. this.members.length = 0; // remove all items from list
  246. // now we remove all items from index reference
  247. Object.keys(this.reference).forEach(function (key) {
  248. delete this.reference[key];
  249. }.bind(this));
  250. },
  251. /**
  252. * Load one or more items
  253. *
  254. * @param {Object|Array} items
  255. */
  256. populate: function (items) {
  257. // if Type supports parsing of string headers then do it before adding it.
  258. _.isString(items) && _.isFunction(this.Type.parse) && (items = this.Type.parse(items));
  259. // add a single item or an array of items.
  260. _.forEach(_.isArray(items) ? items :
  261. // if population is not an array, we send this as single item in an array or send each property separately
  262. // if the core Type supports Type.create
  263. ((_.isPlainObject(items) && _.has(this.Type, 'create')) ? items : [items]), this.add.bind(this));
  264. },
  265. /**
  266. * Clears the list and adds new items.
  267. *
  268. * @param {Object|Array} items
  269. */
  270. repopulate: function (items) {
  271. this.clear();
  272. this.populate(items);
  273. },
  274. /**
  275. * Add or update values from a source list.
  276. *
  277. * @param {PropertyList|Array} source
  278. * @param {Boolean} [prune=false] Setting this to `true` will cause the extra items from the list to be deleted
  279. */
  280. assimilate: function (source, prune) {
  281. var members = PropertyList.isPropertyList(source) ? source.members : source, list = this, indexer = list._postman_listIndexKey, sourceKeys = {}; // keeps track of added / updated keys for later exclusion
  282. if (!_.isArray(members)) {
  283. return;
  284. }
  285. members.forEach(function (item) {
  286. if (!(item && item.hasOwnProperty(indexer))) {
  287. return;
  288. }
  289. list.upsert(item);
  290. sourceKeys[item[indexer]] = true;
  291. });
  292. // now remove any variable that is not in source object
  293. // @note - using direct `this.reference` list of keys here so that we can mutate the list while iterating
  294. // on it
  295. if (prune) {
  296. _.forEach(list.reference, function (value, key) {
  297. if (sourceKeys.hasOwnProperty(key)) {
  298. return;
  299. } // de not delete if source obj has this variable
  300. list.remove(key); // use PropertyList functions to remove so that the .members array is cleared too
  301. });
  302. }
  303. },
  304. /**
  305. * Returns a map of all items.
  306. *
  307. * @returns {Object}
  308. */
  309. all: function () {
  310. return _.clone(this.members);
  311. },
  312. /**
  313. * Get Item in this list by `ID` reference. If multiple values are allowed, the last value is returned.
  314. *
  315. * @param {String} id
  316. * @returns {PropertyList.Type}
  317. */
  318. one: function (id) {
  319. var val = this.reference[this._postman_listIndexCaseInsensitive ? String(id).toLowerCase() : id];
  320. if (this._postman_listAllowsMultipleValues && Array.isArray(val)) {
  321. return val.length ? val[val.length - 1] : undefined;
  322. }
  323. return val;
  324. },
  325. /**
  326. * Get the value of an item in this list. This is similar to {@link PropertyList.one} barring the fact that it
  327. * returns the value of the underlying type of the list content instead of the item itself.
  328. *
  329. * @param {String|Function} key
  330. * @returns {PropertyList.Type|*}
  331. */
  332. get: function (key) {
  333. var member = this.one(key);
  334. if (!member) {
  335. return;
  336. } // eslint-disable-line getter-return
  337. return member.valueOf();
  338. },
  339. /**
  340. * Iterate on each item of this list.
  341. *
  342. * @param {Function} iterator
  343. * @param {Object} context
  344. */
  345. each: function (iterator, context) {
  346. _.forEach(this.members, _.isFunction(iterator) ? iterator.bind(context || this.__parent) : iterator);
  347. },
  348. /**
  349. * @param {Function} rule
  350. * @param {Object} context
  351. */
  352. filter: function (rule, context) {
  353. return _.filter(this.members, _.isFunction(rule) && _.isObject(context) ? rule.bind(context) : rule);
  354. },
  355. /**
  356. * Find an item within the item group
  357. *
  358. * @param {Function} rule
  359. * @param {Object} [context]
  360. * @returns {Item|ItemGroup}
  361. */
  362. find: function (rule, context) {
  363. return _.find(this.members, _.isFunction(rule) && _.isObject(context) ? rule.bind(context) : rule);
  364. },
  365. /**
  366. * Iterates over the property list.
  367. *
  368. * @param {Function} iterator Function to call on each item.
  369. * @param {Object} context Optional context, defaults to the PropertyList itself.
  370. */
  371. map: function (iterator, context) {
  372. return _.map(this.members, _.isFunction(iterator) ? iterator.bind(context || this) : iterator);
  373. },
  374. /**
  375. * Iterates over the property list and accumulates the result.
  376. *
  377. * @param {Function} iterator Function to call on each item.
  378. * @param {*} accumulator Accumulator initial value
  379. * @param {Object} context Optional context, defaults to the PropertyList itself.
  380. */
  381. reduce: function (iterator, accumulator, context) {
  382. return _.reduce(this.members, _.isFunction(iterator) ? iterator.bind(context || this) : iterator, accumulator);
  383. },
  384. /**
  385. * Returns the length of the PropertyList
  386. *
  387. * @returns {Number}
  388. */
  389. count: function () {
  390. return this.members.length;
  391. },
  392. /**
  393. * Get a member of this list by it's index
  394. *
  395. * @param {Number} index
  396. * @returns {PropertyList.Type}
  397. */
  398. idx: function (index) {
  399. return this.members[index];
  400. },
  401. /**
  402. * Find the index of an item in this list
  403. *
  404. * @param {String|Object} item
  405. * @returns {Number}
  406. */
  407. indexOf: function (item) {
  408. return this.members.indexOf(_.isString(item) ? (item = this.one(item)) : item);
  409. },
  410. /**
  411. * Check whether an item exists in this list
  412. *
  413. * @param {String|PropertyList.Type} item
  414. * @param {*=} value
  415. * @returns {Boolean}
  416. */
  417. has: function (item, value) {
  418. var match, val, i;
  419. match = _.isString(item) ?
  420. this.reference[this._postman_listIndexCaseInsensitive ? item.toLowerCase() : item] :
  421. this.filter(function (member) {
  422. return member === item;
  423. });
  424. // If we don't have a match, there's nothing to do
  425. if (!match) {
  426. return false;
  427. }
  428. // if no value is provided, just check if item exists
  429. if (arguments.length === 1) {
  430. return Boolean(_.isArray(match) ? match.length : match);
  431. }
  432. // If this property allows multiple values and we get an array, we need to iterate through it and see
  433. // if any element matches.
  434. if (this._postman_listAllowsMultipleValues && _.isArray(match)) {
  435. for (i = 0; i < match.length; i++) {
  436. // use the value of the current element
  437. val = _.isFunction(match[i].valueOf) ? match[i].valueOf() : match[i];
  438. if (val === value) {
  439. return true;
  440. }
  441. }
  442. // no matches were found, so return false here.
  443. return false;
  444. }
  445. // We didn't have an array, so just check if the matched value equals the provided value.
  446. _.isFunction(match.valueOf) && (match = match.valueOf());
  447. return match === value;
  448. },
  449. /**
  450. * Iterates over all parents of the property list
  451. *
  452. * @param {Function} iterator
  453. * @param {Object=} [context]
  454. */
  455. eachParent: function (iterator, context) {
  456. // validate parameters
  457. if (!_.isFunction(iterator)) {
  458. return;
  459. }
  460. !context && (context = this);
  461. var parent = this.__parent, prev;
  462. // iterate till there is no parent
  463. while (parent) {
  464. // call iterator with the parent and previous parent
  465. iterator.call(context, parent, prev);
  466. // update references
  467. prev = parent;
  468. parent = parent.__parent;
  469. }
  470. },
  471. /**
  472. * Converts a list of Properties into an object where key is `_postman_propertyIndexKey` and value is determined
  473. * by the `valueOf` function
  474. *
  475. * @param {?Boolean} [excludeDisabled=false] - When set to true, disabled properties are excluded from the resultant
  476. * object.
  477. * @param {?Boolean} [caseSensitive] - When set to true, properties are treated strictly as per their original
  478. * case. The default value for this property also depends on the case insensitivity definition of the current
  479. * property.
  480. * @param {?Boolean} [multiValue=false] - When set to true, only the first value of a multi valued property is
  481. * returned.
  482. * @param {Boolean} [sanitizeKeys=false] - When set to true, properties with falsy keys are removed.
  483. * @todo Change the function signature to an object of options instead of the current structure.
  484. * @return {Object}
  485. */
  486. toObject: function (excludeDisabled, caseSensitive, multiValue, sanitizeKeys) {
  487. var obj = {}, // create transformation data accumulator
  488. // gather all the switches of the list
  489. key = this._postman_listIndexKey, sanitiseKeys = this._postman_sanitizeKeys || sanitizeKeys, sensitive = !this._postman_listIndexCaseInsensitive || caseSensitive, multivalue = this._postman_listAllowsMultipleValues || multiValue;
  490. // iterate on each member to create the transformation object
  491. this.each(function (member) {
  492. // Bail out for the current member if ANY of the conditions below is true:
  493. // 1. The member is falsy.
  494. // 2. The member does not have the specified property list index key.
  495. // 3. The member is disabled and disabled properties have to be ignored.
  496. // 4. The member has a falsy key, and sanitize is true.
  497. if (!member || !member.hasOwnProperty(key) || (excludeDisabled && member.disabled) ||
  498. (sanitiseKeys && !member[key])) {
  499. return;
  500. }
  501. // based on case sensitivity settings, we get the property name of the item
  502. var prop = sensitive ? member[key] : String(member[key]).toLowerCase();
  503. // now, if transformation object already has a member with same property name, we either overwrite it or
  504. // append to an array of values based on multi-value support
  505. if (multivalue && obj.hasOwnProperty(prop)) {
  506. (!Array.isArray(obj[prop])) && (obj[prop] = [obj[prop]]);
  507. obj[prop].push(member.valueOf());
  508. }
  509. else {
  510. obj[prop] = member.valueOf();
  511. }
  512. });
  513. return obj;
  514. },
  515. /**
  516. * Adds ability to convert a list to a string provided it's underlying format has unparse function defined.
  517. *
  518. * @return {String}
  519. */
  520. toString: function () {
  521. if (this.Type.unparse) {
  522. return this.Type.unparse(this.members);
  523. }
  524. return this.constructor ? this.constructor.prototype.toString.call(this) : '';
  525. },
  526. toJSON: function () {
  527. if (!this.count()) {
  528. return [];
  529. }
  530. return _.map(this.members, function (member) {
  531. // use member.toJSON if it exists
  532. if (!_.isEmpty(member) && _.isFunction(member.toJSON)) {
  533. return member.toJSON();
  534. }
  535. return _.reduce(member, function (accumulator, value, key) {
  536. if (value === undefined) { // true/false/null need to be preserved.
  537. return accumulator;
  538. }
  539. // Handle plurality of PropertyLists in the SDK vs the exported JSON.
  540. // Basically, removes the trailing "s" from key if the value is a property list.
  541. if (value && value._postman_propertyIsList && !value._postman_proprtyIsSerialisedAsPlural &&
  542. _.endsWith(key, 's')) {
  543. key = key.slice(0, -1);
  544. }
  545. // Handle 'PropertyBase's
  546. if (value && _.isFunction(value.toJSON)) {
  547. accumulator[key] = value.toJSON();
  548. return accumulator;
  549. }
  550. // Handle Strings
  551. if (_.isString(value)) {
  552. accumulator[key] = value;
  553. return accumulator;
  554. }
  555. // Everything else
  556. accumulator[key] = _.cloneElement(value);
  557. return accumulator;
  558. }, {});
  559. });
  560. }
  561. });
  562. _.assign(PropertyList, /** @lends PropertyList */ {
  563. /**
  564. * Defines the name of this property for internal use.
  565. * @private
  566. * @readOnly
  567. * @type {String}
  568. */
  569. _postman_propertyName: 'PropertyList',
  570. /**
  571. * Removes child-parent links for the provided PropertyList member.
  572. *
  573. * @param {Property} item - The property for which to perform parent de-linking.
  574. * @private
  575. */
  576. _unlinkItemFromParent: function (item) {
  577. item.__parent && (delete item.__parent); // prevents V8 from making unnecessary look-ups if there is no __parent
  578. },
  579. /**
  580. * Checks whether an object is a PropertyList
  581. *
  582. * @param {*} obj
  583. * @returns {Boolean}
  584. */
  585. isPropertyList: function (obj) {
  586. return Boolean(obj) && ((obj instanceof PropertyList) ||
  587. _.inSuperChain(obj.constructor, '_postman_propertyName', PropertyList._postman_propertyName));
  588. }
  589. });
  590. module.exports = {
  591. PropertyList: PropertyList
  592. };