project-data.spec.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. import {expect} from 'chai';
  2. import {format} from '@e22m4u/js-format';
  3. import {projectData} from './project-data.js';
  4. describe('projectData', function () {
  5. it('should require the parameter "schemaOrFactory" to be a valid value', function () {
  6. const throwable = v => () => projectData(v, {});
  7. const error = s =>
  8. format(
  9. 'Projection schema must be an Object, a Function, ' +
  10. 'a non-empty String or a Symbol, but %s was given.',
  11. s,
  12. );
  13. expect(throwable('')).to.throw(error('""'));
  14. expect(throwable(10)).to.throw(error('10'));
  15. expect(throwable(0)).to.throw(error('0'));
  16. expect(throwable(true)).to.throw(error('true'));
  17. expect(throwable(false)).to.throw(error('false'));
  18. expect(throwable([])).to.throw(error('Array'));
  19. expect(throwable(null)).to.throw(error('null'));
  20. expect(throwable(undefined)).to.throw(error('undefined'));
  21. projectData({}, {});
  22. projectData(() => ({}), {});
  23. projectData('mySchema', {}, {resolver: () => ({})});
  24. });
  25. it('should require the parameter "options" to be an object', function () {
  26. const throwable = v => () => projectData({}, {}, v);
  27. const error = s =>
  28. format('Parameter "options" must be an Object, but %s was given.', s);
  29. expect(throwable('str')).to.throw(error('"str"'));
  30. expect(throwable('')).to.throw(error('""'));
  31. expect(throwable(10)).to.throw(error('10'));
  32. expect(throwable(0)).to.throw(error('0'));
  33. expect(throwable(true)).to.throw(error('true'));
  34. expect(throwable(false)).to.throw(error('false'));
  35. expect(throwable([])).to.throw(error('Array'));
  36. expect(throwable(null)).to.throw(error('null'));
  37. throwable({})();
  38. throwable(undefined)();
  39. });
  40. it('should require the option "strict" to be a boolean', function () {
  41. const throwable = v => () => projectData({}, {}, {strict: v});
  42. const error = s =>
  43. format('Option "strict" must be a Boolean, but %s was given.', s);
  44. expect(throwable('str')).to.throw(error('"str"'));
  45. expect(throwable('')).to.throw(error('""'));
  46. expect(throwable(10)).to.throw(error('10'));
  47. expect(throwable(0)).to.throw(error('0'));
  48. expect(throwable([])).to.throw(error('Array'));
  49. expect(throwable({})).to.throw(error('Object'));
  50. expect(throwable(null)).to.throw(error('null'));
  51. throwable(true)();
  52. throwable(false)();
  53. throwable(undefined)();
  54. });
  55. it('should require the option "scope" to be a non-empty string', function () {
  56. const throwable = v => () => projectData({}, {}, {scope: v});
  57. const error = s =>
  58. format('Option "scope" must be a non-empty String, but %s was given.', s);
  59. expect(throwable('')).to.throw(error('""'));
  60. expect(throwable(10)).to.throw(error('10'));
  61. expect(throwable(0)).to.throw(error('0'));
  62. expect(throwable(true)).to.throw(error('true'));
  63. expect(throwable(false)).to.throw(error('false'));
  64. expect(throwable([])).to.throw(error('Array'));
  65. expect(throwable({})).to.throw(error('Object'));
  66. expect(throwable(null)).to.throw(error('null'));
  67. throwable('str')();
  68. throwable(undefined)();
  69. });
  70. it('should require the option "resolver" to be a Function', function () {
  71. const throwable = v => () => projectData({}, {}, {resolver: v});
  72. const error = s =>
  73. format('Option "resolver" must be a Function, but %s was given.', s);
  74. expect(throwable('')).to.throw(error('""'));
  75. expect(throwable(10)).to.throw(error('10'));
  76. expect(throwable(0)).to.throw(error('0'));
  77. expect(throwable(true)).to.throw(error('true'));
  78. expect(throwable(false)).to.throw(error('false'));
  79. expect(throwable([])).to.throw(error('Array'));
  80. expect(throwable({})).to.throw(error('Object'));
  81. expect(throwable(null)).to.throw(error('null'));
  82. throwable(() => undefined)();
  83. throwable(undefined)();
  84. });
  85. it('should return a non-object and non-array data as is', function () {
  86. const fn = v => projectData({foo: true}, v);
  87. expect(fn('str')).to.be.eql('str');
  88. expect(fn('')).to.be.eql('');
  89. expect(fn(10)).to.be.eql(10);
  90. expect(fn(0)).to.be.eql(0);
  91. expect(fn(true)).to.be.eql(true);
  92. expect(fn(false)).to.be.eql(false);
  93. expect(fn(null)).to.be.eql(null);
  94. expect(fn(undefined)).to.be.eql(undefined);
  95. });
  96. it('should apply projection for each array element', function () {
  97. const schema = {foo: true, bar: false};
  98. const data = [
  99. {foo: 10, bar: 20, baz: 30},
  100. {foo: 40, bar: 50, baz: 60},
  101. ];
  102. const res = projectData(schema, data);
  103. expect(res).to.be.eql([
  104. {foo: 10, baz: 30},
  105. {foo: 40, baz: 60},
  106. ]);
  107. });
  108. it('should add fields without rules by default', function () {
  109. const res = projectData({}, {foo: 10, bar: 20});
  110. expect(res).to.be.eql({foo: 10, bar: 20});
  111. });
  112. it('should project fields by a boolean value', function () {
  113. const res = projectData({foo: true, bar: false}, {foo: 10, bar: 20});
  114. expect(res).to.be.eql({foo: 10});
  115. });
  116. it('should project fields by the select option', function () {
  117. const res = projectData(
  118. {foo: {select: true}, bar: {select: false}},
  119. {foo: 10, bar: 20},
  120. );
  121. expect(res).to.be.eql({foo: 10});
  122. });
  123. it('should ignore scope-related rules by default', function () {
  124. const res = projectData(
  125. {foo: {scopes: {input: false, output: false}}},
  126. {foo: 10},
  127. );
  128. expect(res).to.be.eql({foo: 10});
  129. });
  130. it('should create nested projection by the schema option', function () {
  131. const res = projectData(
  132. {foo: true, bar: false, qux: {schema: {abc: true, def: false}}},
  133. {foo: 10, bar: 20, qux: {abc: 30, def: 40}},
  134. );
  135. expect(res).to.be.eql({foo: 10, qux: {abc: 30}});
  136. });
  137. describe('schema factory', function () {
  138. it('should throw an error if the schema factory returns an invalid value', function () {
  139. const throwable = v => () => projectData(() => v, {});
  140. const error = s =>
  141. format(
  142. 'Projection schema factory must return an Object, ' +
  143. 'a non-empty String or a Symbol, but %s was given.',
  144. s,
  145. );
  146. expect(throwable('')).to.throw(error('""'));
  147. expect(throwable(10)).to.throw(error('10'));
  148. expect(throwable(0)).to.throw(error('0'));
  149. expect(throwable(true)).to.throw(error('true'));
  150. expect(throwable(false)).to.throw(error('false'));
  151. expect(throwable([])).to.throw(error('Array'));
  152. expect(throwable(null)).to.throw(error('null'));
  153. expect(throwable(undefined)).to.throw(error('undefined'));
  154. projectData(() => ({}), {});
  155. projectData(() => 'mySchema', {}, {resolver: () => ({})});
  156. projectData(() => Symbol('mySchema'), {}, {resolver: () => ({})});
  157. });
  158. it('should resolve a schema object from the given factory', function () {
  159. let invoked = 0;
  160. const factory = () => {
  161. invoked++;
  162. return {foo: true, bar: false};
  163. };
  164. const res = projectData(factory, {foo: 10, bar: 20});
  165. expect(res).to.be.eql({foo: 10});
  166. expect(invoked).to.be.eq(1);
  167. });
  168. it('should use the schema factory in the nested schema', function () {
  169. let invoked = 0;
  170. const factory = () => {
  171. invoked++;
  172. return {baz: true, qux: false};
  173. };
  174. const res = projectData(
  175. {foo: true, bar: {schema: factory}},
  176. {foo: 10, bar: {baz: 20, qux: 30}},
  177. );
  178. expect(res).to.be.eql({foo: 10, bar: {baz: 20}});
  179. expect(invoked).to.be.eq(1);
  180. });
  181. });
  182. describe('string key', function () {
  183. it('should throw an error if the schema resolver returns an invalid value', function () {
  184. const throwable = v => () =>
  185. projectData('mySchema', {}, {resolver: () => v});
  186. const error = s =>
  187. format(
  188. 'Projection schema resolver must return an Object, but %s was given.',
  189. s,
  190. );
  191. expect(throwable('str')).to.throw(error('"str"'));
  192. expect(throwable('')).to.throw(error('""'));
  193. expect(throwable(10)).to.throw(error('10'));
  194. expect(throwable(0)).to.throw(error('0'));
  195. expect(throwable(true)).to.throw(error('true'));
  196. expect(throwable(false)).to.throw(error('false'));
  197. expect(throwable([])).to.throw(error('Array'));
  198. expect(throwable(null)).to.throw(error('null'));
  199. expect(throwable(undefined)).to.throw(error('undefined'));
  200. throwable({})();
  201. });
  202. it('should throw an error if no schema resolver is provided when a string key is given', function () {
  203. const throwable = () => projectData('mySchema', {});
  204. expect(throwable).to.throw(
  205. 'Unable to resolve the projection schema "mySchema" ' +
  206. 'without a provided resolver.',
  207. );
  208. });
  209. it('should pass the string key to the schema resolver and project the given data', function () {
  210. let invoked = 0;
  211. const resolver = key => {
  212. expect(key).to.be.eq('mySchema');
  213. invoked++;
  214. return {foo: true, bar: false};
  215. };
  216. const res = projectData('mySchema', {foo: 10, bar: 20}, {resolver});
  217. expect(res).to.be.eql({foo: 10});
  218. expect(invoked).to.be.eq(1);
  219. });
  220. it('should use the schema resolver in the nested schema', function () {
  221. let invoked = 0;
  222. const resolver = key => {
  223. expect(key).to.be.eq('mySchema');
  224. invoked++;
  225. return {baz: true, qux: false};
  226. };
  227. const res = projectData(
  228. {foo: true, bar: {schema: 'mySchema'}},
  229. {foo: 10, bar: {baz: 20, qux: 30}},
  230. {resolver},
  231. );
  232. expect(res).to.be.eql({foo: 10, bar: {baz: 20}});
  233. expect(invoked).to.be.eq(1);
  234. });
  235. it('should resolve the string key from the schema factory', function () {
  236. let invoked = 0;
  237. const resolver = key => {
  238. expect(key).to.be.eq('mySchema');
  239. invoked++;
  240. return {foo: true, bar: false};
  241. };
  242. const res = projectData(() => 'mySchema', {foo: 10, bar: 20}, {resolver});
  243. expect(res).to.be.eql({foo: 10});
  244. expect(invoked).to.be.eq(1);
  245. });
  246. });
  247. describe('symbol key', function () {
  248. it('should throw an error if the schema resolver returns an invalid value', function () {
  249. const throwable = v => () =>
  250. projectData(Symbol('mySchema'), {}, {resolver: () => v});
  251. const error = s =>
  252. format(
  253. 'Projection schema resolver must return an Object, but %s was given.',
  254. s,
  255. );
  256. expect(throwable('str')).to.throw(error('"str"'));
  257. expect(throwable('')).to.throw(error('""'));
  258. expect(throwable(10)).to.throw(error('10'));
  259. expect(throwable(0)).to.throw(error('0'));
  260. expect(throwable(true)).to.throw(error('true'));
  261. expect(throwable(false)).to.throw(error('false'));
  262. expect(throwable([])).to.throw(error('Array'));
  263. expect(throwable(null)).to.throw(error('null'));
  264. expect(throwable(undefined)).to.throw(error('undefined'));
  265. throwable({})();
  266. });
  267. it('should throw an error if no schema resolver is provided when a symbol key is given', function () {
  268. const throwable = () => projectData(Symbol('mySchema'), {});
  269. expect(throwable).to.throw(
  270. 'Unable to resolve the projection schema Symbol("mySchema") ' +
  271. 'without a provided resolver.',
  272. );
  273. });
  274. it('should pass the symbol key to the schema resolver and project the given data', function () {
  275. let invoked = 0;
  276. const symbolKey = Symbol('mySchema');
  277. const resolver = key => {
  278. expect(key).to.be.eq(symbolKey);
  279. invoked++;
  280. return {foo: true, bar: false};
  281. };
  282. const res = projectData(symbolKey, {foo: 10, bar: 20}, {resolver});
  283. expect(res).to.be.eql({foo: 10});
  284. expect(invoked).to.be.eq(1);
  285. });
  286. it('should use the schema resolver in the nested schema', function () {
  287. let invoked = 0;
  288. const symbolKey = Symbol('mySchema');
  289. const resolver = key => {
  290. expect(key).to.be.eq(symbolKey);
  291. invoked++;
  292. return {baz: true, qux: false};
  293. };
  294. const res = projectData(
  295. {foo: true, bar: {schema: symbolKey}},
  296. {foo: 10, bar: {baz: 20, qux: 30}},
  297. {resolver},
  298. );
  299. expect(res).to.be.eql({foo: 10, bar: {baz: 20}});
  300. expect(invoked).to.be.eq(1);
  301. });
  302. it('should resolve the symbol key from the schema factory', function () {
  303. let invoked = 0;
  304. const symbolKey = Symbol('mySchema');
  305. const resolver = key => {
  306. expect(key).to.be.eq(symbolKey);
  307. invoked++;
  308. return {foo: true, bar: false};
  309. };
  310. const res = projectData(() => symbolKey, {foo: 10, bar: 20}, {resolver});
  311. expect(res).to.be.eql({foo: 10});
  312. expect(invoked).to.be.eq(1);
  313. });
  314. });
  315. describe('strict mode', function () {
  316. it('should preserve fields not defined in the schema when the strict option is false', function () {
  317. const res = projectData({}, {foo: 10});
  318. expect(res).to.be.eql({foo: 10});
  319. });
  320. it('should remove fields without rules when the strict mode is enabled', function () {
  321. const res = projectData({}, {foo: 10}, {strict: true});
  322. expect(res).to.be.eql({});
  323. });
  324. it('should project fields by a boolean value', function () {
  325. const res = projectData(
  326. {foo: true, bar: false},
  327. {foo: 10, bar: 20},
  328. {strict: true},
  329. );
  330. expect(res).to.be.eql({foo: 10});
  331. });
  332. it('should project fields by the select option', function () {
  333. const res = projectData(
  334. {foo: {select: true}, bar: {select: false}},
  335. {foo: 10, bar: 20},
  336. {strict: true},
  337. );
  338. expect(res).to.be.eql({foo: 10});
  339. });
  340. it('should propagate the strict mode to nested schema', function () {
  341. const res = projectData(
  342. {foo: false, bar: {select: true, schema: {baz: true}}},
  343. {foo: 10, bar: {baz: 20, qux: 30}},
  344. {strict: true},
  345. );
  346. expect(res).to.be.eql({bar: {baz: 20}});
  347. });
  348. it('should ignore prototype properties', function () {
  349. const res = projectData(
  350. {bar: true, toString: true},
  351. {bar: 10},
  352. {strict: true},
  353. );
  354. expect(res).to.be.eql({bar: 10});
  355. });
  356. });
  357. describe('projection scope', function () {
  358. it('should apply scope-specific selection rule by a boolean value', function () {
  359. const schema = {
  360. foo: {
  361. select: false,
  362. scopes: {
  363. input: true,
  364. },
  365. },
  366. bar: true,
  367. };
  368. const data = {foo: 10, bar: 20};
  369. const res1 = projectData(schema, data);
  370. const res2 = projectData(schema, data, {scope: 'input'});
  371. expect(res1).to.be.eql({bar: 20});
  372. expect(res2).to.be.eql({foo: 10, bar: 20});
  373. });
  374. it('should apply scope-specific selection rule by the select option', function () {
  375. const schema = {
  376. foo: {
  377. select: false,
  378. scopes: {
  379. input: {select: true},
  380. },
  381. },
  382. bar: {select: true},
  383. };
  384. const data = {foo: 10, bar: 20};
  385. const res1 = projectData(schema, data);
  386. const res2 = projectData(schema, data, {scope: 'input'});
  387. expect(res1).to.be.eql({bar: 20});
  388. expect(res2).to.be.eql({foo: 10, bar: 20});
  389. });
  390. it('should fallback to general rule if scope rule is missing', function () {
  391. const schema = {
  392. foo: {
  393. select: true,
  394. scopes: {
  395. output: {select: false},
  396. },
  397. },
  398. };
  399. const data = {foo: 10};
  400. const res = projectData(schema, data, {scope: 'input'});
  401. expect(res).to.be.eql({foo: 10});
  402. });
  403. it('should fallback to the general rule if the scope options exists but lacks the select option', function () {
  404. const schema = {
  405. foo: {
  406. select: true,
  407. scopes: {
  408. input: {},
  409. },
  410. },
  411. bar: {
  412. select: false,
  413. scopes: {
  414. input: {},
  415. },
  416. },
  417. };
  418. const data = {foo: 10, bar: 20};
  419. const res = projectData(schema, data, {scope: 'input'});
  420. expect(res).to.be.eql({foo: 10});
  421. });
  422. });
  423. });