project-data.spec.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  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 "options" argument to be an object', function () {
  6. const throwable = v => () => projectData(10, {}, v);
  7. const error = s =>
  8. format('Projection options must be an Object, but %s was given.', s);
  9. expect(throwable('str')).to.throw(error('"str"'));
  10. expect(throwable('')).to.throw(error('""'));
  11. expect(throwable(10)).to.throw(error('10'));
  12. expect(throwable(0)).to.throw(error('0'));
  13. expect(throwable(true)).to.throw(error('true'));
  14. expect(throwable(false)).to.throw(error('false'));
  15. expect(throwable([])).to.throw(error('Array'));
  16. expect(throwable(null)).to.throw(error('null'));
  17. throwable({})();
  18. throwable(undefined)();
  19. });
  20. it('should require the "strict" option to be a boolean', function () {
  21. const throwable = v => () => projectData(10, {}, {strict: v});
  22. const error = s =>
  23. format(
  24. 'Projection option "strict" must be a Boolean, but %s was given.',
  25. s,
  26. );
  27. expect(throwable('str')).to.throw(error('"str"'));
  28. expect(throwable('')).to.throw(error('""'));
  29. expect(throwable(10)).to.throw(error('10'));
  30. expect(throwable(0)).to.throw(error('0'));
  31. expect(throwable([])).to.throw(error('Array'));
  32. expect(throwable({})).to.throw(error('Object'));
  33. expect(throwable(null)).to.throw(error('null'));
  34. throwable(true)();
  35. throwable(false)();
  36. throwable(undefined)();
  37. });
  38. it('should require the "scope" option to be a non-empty string', function () {
  39. const throwable = v => () => projectData(10, {}, {scope: v});
  40. const error = s =>
  41. format(
  42. 'Projection option "scope" must be a non-empty String, ' +
  43. 'but %s was given.',
  44. s,
  45. );
  46. expect(throwable('')).to.throw(error('""'));
  47. expect(throwable(10)).to.throw(error('10'));
  48. expect(throwable(0)).to.throw(error('0'));
  49. expect(throwable(true)).to.throw(error('true'));
  50. expect(throwable(false)).to.throw(error('false'));
  51. expect(throwable([])).to.throw(error('Array'));
  52. expect(throwable({})).to.throw(error('Object'));
  53. expect(throwable(null)).to.throw(error('null'));
  54. throwable('str')();
  55. throwable(undefined)();
  56. });
  57. it('should require the "nameResolver" option to be a function', function () {
  58. const throwable = v => () => projectData(10, {}, {nameResolver: v});
  59. const error = s =>
  60. format(
  61. 'Projection option "nameResolver" must be ' +
  62. 'a Function, but %s was given.',
  63. s,
  64. );
  65. expect(throwable('str')).to.throw(error('"str"'));
  66. expect(throwable('')).to.throw(error('""'));
  67. expect(throwable(10)).to.throw(error('10'));
  68. expect(throwable(0)).to.throw(error('0'));
  69. expect(throwable(true)).to.throw(error('true'));
  70. expect(throwable(false)).to.throw(error('false'));
  71. expect(throwable([])).to.throw(error('Array'));
  72. expect(throwable({})).to.throw(error('Object'));
  73. expect(throwable(null)).to.throw(error('null'));
  74. throwable(() => ({}))();
  75. throwable(undefined)();
  76. });
  77. it('should require the "factoryArgs" option to be an Array', function () {
  78. const throwable = v => () => projectData(10, {}, {factoryArgs: v});
  79. const error = s =>
  80. format(
  81. 'Projection option "factoryArgs" must be an Array, but %s was given.',
  82. s,
  83. );
  84. expect(throwable('str')).to.throw(error('"str"'));
  85. expect(throwable('')).to.throw(error('""'));
  86. expect(throwable(10)).to.throw(error('10'));
  87. expect(throwable(0)).to.throw(error('0'));
  88. expect(throwable(true)).to.throw(error('true'));
  89. expect(throwable(false)).to.throw(error('false'));
  90. expect(throwable({})).to.throw(error('Object'));
  91. expect(throwable(null)).to.throw(error('null'));
  92. throwable([1])();
  93. throwable([])();
  94. });
  95. it('should resolve a factory value as the schema object', function () {
  96. let invoked = 0;
  97. const factory = () => {
  98. invoked++;
  99. return {foo: true, bar: false};
  100. };
  101. const res = projectData({foo: 10, bar: 20}, factory);
  102. expect(res).to.be.eql({foo: 10});
  103. expect(invoked).to.be.eq(1);
  104. });
  105. it('should pass the "factoryArgs" option to the schema factory', function () {
  106. let invoked = 0;
  107. const factoryArgs = [1, 2, 3];
  108. const factory = (...args) => {
  109. invoked++;
  110. expect(args).to.be.eql(factoryArgs);
  111. return {foo: true, bar: false};
  112. };
  113. const res = projectData({foo: 10, bar: 20}, factory, {factoryArgs});
  114. expect(res).to.be.eql({foo: 10});
  115. expect(invoked).to.be.eq(1);
  116. });
  117. it('should pass the "factoryArgs" option to the schema factory in the nested schema', function () {
  118. let invoked = 0;
  119. const factoryArgs = [1, 2, 3];
  120. const factory = (...args) => {
  121. invoked++;
  122. expect(args).to.be.eql(factoryArgs);
  123. return {bar: true, baz: false};
  124. };
  125. const res = projectData(
  126. {foo: {bar: 10, baz: 20}},
  127. {foo: {schema: factory}},
  128. {factoryArgs},
  129. );
  130. expect(res).to.be.eql({foo: {bar: 10}});
  131. expect(invoked).to.be.eq(1);
  132. });
  133. it('should require a factory value to be an object or a non-empty string', function () {
  134. const throwable = v => () => projectData({}, () => v);
  135. const error = s =>
  136. format(
  137. 'Schema factory must return an Object ' +
  138. 'or a non-empty String, but %s was given.',
  139. s,
  140. );
  141. expect(throwable('')).to.throw(error('""'));
  142. expect(throwable(10)).to.throw(error('10'));
  143. expect(throwable(0)).to.throw(error('0'));
  144. expect(throwable(true)).to.throw(error('true'));
  145. expect(throwable(false)).to.throw(error('false'));
  146. expect(throwable([])).to.throw(error('Array'));
  147. expect(throwable(undefined)).to.throw(error('undefined'));
  148. expect(throwable(null)).to.throw(error('null'));
  149. expect(throwable(() => undefined)).to.throw(error('Function'));
  150. projectData({}, () => ({}));
  151. projectData({}, () => 'str', {nameResolver: () => ({})});
  152. });
  153. it('should resolve the schema name by the name resolver', function () {
  154. let invoked = 0;
  155. const nameResolver = name => {
  156. invoked++;
  157. expect(name).to.be.eql('mySchema');
  158. return {foo: true, bar: false};
  159. };
  160. const res = projectData({foo: 10, bar: 20}, 'mySchema', {nameResolver});
  161. expect(res).to.be.eql({foo: 10});
  162. expect(invoked).to.be.eq(1);
  163. });
  164. it('should require the "nameResolver" option when the schema name is provided', function () {
  165. const throwable = () => projectData({}, 'mySchema');
  166. expect(throwable).to.throw(
  167. 'Projection option "nameResolver" is required ' +
  168. 'to resolve "mySchema" name.',
  169. );
  170. });
  171. it('should require the name resolver to return an object', function () {
  172. const throwable = v => () =>
  173. projectData({}, 'mySchema', {nameResolver: () => v});
  174. const error = s =>
  175. format(
  176. 'Name resolver must return an Object, a Function ' +
  177. 'or a non-empty String, but %s was given.',
  178. s,
  179. );
  180. expect(throwable('')).to.throw(error('""'));
  181. expect(throwable(10)).to.throw(error('10'));
  182. expect(throwable(0)).to.throw(error('0'));
  183. expect(throwable(true)).to.throw(error('true'));
  184. expect(throwable(false)).to.throw(error('false'));
  185. expect(throwable([])).to.throw(error('Array'));
  186. expect(throwable(undefined)).to.throw(error('undefined'));
  187. expect(throwable(null)).to.throw(error('null'));
  188. throwable({})();
  189. });
  190. it('should resolve the schema name from the factory function', function () {
  191. let invoked = 0;
  192. const nameResolver = name => {
  193. invoked++;
  194. expect(name).to.be.eql('mySchema');
  195. return {foo: true, bar: false};
  196. };
  197. const res = projectData({foo: 10, bar: 20}, () => 'mySchema', {
  198. nameResolver,
  199. });
  200. expect(res).to.be.eql({foo: 10});
  201. expect(invoked).to.be.eq(1);
  202. });
  203. it('should resolve the schema name in the the nested object', function () {
  204. let invoked = 0;
  205. const nameResolver = name => {
  206. invoked++;
  207. if (name === 'schema1') {
  208. return {foo: true, bar: {schema: 'schema2'}};
  209. } else if (name === 'schema2') {
  210. return {baz: true, qux: false};
  211. }
  212. };
  213. const data = {foo: 10, bar: {baz: 20, qux: 30}};
  214. const res = projectData(data, 'schema1', {nameResolver});
  215. expect(res).to.be.eql({foo: 10, bar: {baz: 20}});
  216. expect(invoked).to.be.eq(2);
  217. });
  218. it('should resolve the schema object through the name chain', function () {
  219. let invoked = 0;
  220. const nameResolver = name => {
  221. invoked++;
  222. if (name === 'schema1') {
  223. return 'schema2';
  224. } else if (name === 'schema2') {
  225. return {foo: true, bar: false};
  226. }
  227. throw new Error('Invalid argument.');
  228. };
  229. const res = projectData({foo: 10, bar: 20}, 'schema1', {nameResolver});
  230. expect(res).to.be.eql({foo: 10});
  231. expect(invoked).to.be.eq(2);
  232. });
  233. it('should validate a resolved value from the name chain', function () {
  234. const nameResolver = v => name => {
  235. if (name === 'schema1') {
  236. return 'schema2';
  237. } else if (name === 'schema2') {
  238. return v;
  239. } else if (name === 'schema3') {
  240. return {};
  241. }
  242. throw new Error('Invalid argument.');
  243. };
  244. const throwable = v => () =>
  245. projectData({foo: 10, bar: 20}, 'schema1', {
  246. nameResolver: nameResolver(v),
  247. });
  248. const error = s =>
  249. format(
  250. 'Name resolver must return an Object, a Function ' +
  251. 'or a non-empty String, but %s was given.',
  252. s,
  253. );
  254. expect(throwable('')).to.throw(error('""'));
  255. expect(throwable(10)).to.throw(error('10'));
  256. expect(throwable(0)).to.throw(error('0'));
  257. expect(throwable(true)).to.throw(error('true'));
  258. expect(throwable(false)).to.throw(error('false'));
  259. expect(throwable([])).to.throw(error('Array'));
  260. expect(throwable(undefined)).to.throw(error('undefined'));
  261. expect(throwable(null)).to.throw(error('null'));
  262. throwable('schema3')();
  263. });
  264. it('should resolve schema names through the factory function', function () {
  265. let invoked = 0;
  266. const nameResolver = name => {
  267. invoked++;
  268. if (name === 'schema1') {
  269. return () => 'schema2';
  270. } else if (name === 'schema2') {
  271. return {foo: true, bar: false};
  272. }
  273. throw new Error('Invalid argument.');
  274. };
  275. const res = projectData({foo: 10, bar: 20}, 'schema1', {nameResolver});
  276. expect(res).to.be.eql({foo: 10});
  277. expect(invoked).to.be.eq(2);
  278. });
  279. it('should validate a return value of the factory resolved through the name chain', function () {
  280. const nameResolver = v => name => {
  281. if (name === 'schema1') {
  282. return 'schema2';
  283. } else if (name === 'schema2') {
  284. return () => v;
  285. } else if (name === 'schema3') {
  286. return {};
  287. }
  288. throw new Error('Invalid argument.');
  289. };
  290. const throwable = v => () =>
  291. projectData({foo: 10, bar: 20}, 'schema1', {
  292. nameResolver: nameResolver(v),
  293. });
  294. const error = s =>
  295. format(
  296. 'Schema factory must return an Object ' +
  297. 'or a non-empty String, but %s was given.',
  298. s,
  299. );
  300. expect(throwable('')).to.throw(error('""'));
  301. expect(throwable(10)).to.throw(error('10'));
  302. expect(throwable(0)).to.throw(error('0'));
  303. expect(throwable(true)).to.throw(error('true'));
  304. expect(throwable(false)).to.throw(error('false'));
  305. expect(throwable([])).to.throw(error('Array'));
  306. expect(throwable(undefined)).to.throw(error('undefined'));
  307. expect(throwable(null)).to.throw(error('null'));
  308. throwable('schema3')();
  309. });
  310. it('should validate the given schema in the shallow mode', function () {
  311. const schema1 = {foo: '?'};
  312. const schema2 = {foo: true, bar: {schema: {baz: '?'}}};
  313. expect(() => projectData({foo: 10}, schema1)).to.throw(
  314. 'Property options must be an Object or a Boolean, but "?" was given.',
  315. );
  316. const res = projectData({foo: 10}, schema2);
  317. expect(res).to.be.eql({foo: 10});
  318. expect(() => projectData({bar: {baz: 20}}, schema2)).to.throw(
  319. 'Property options must be an Object or a Boolean, but "?" was given.',
  320. );
  321. });
  322. it('should return primitive value as is', function () {
  323. expect(projectData('str', {})).to.be.eq('str');
  324. expect(projectData('', {})).to.be.eq('');
  325. expect(projectData(10, {})).to.be.eq(10);
  326. expect(projectData(0, {})).to.be.eq(0);
  327. expect(projectData(true, {})).to.be.eq(true);
  328. expect(projectData(false, {})).to.be.eq(false);
  329. expect(projectData(undefined, {})).to.be.eq(undefined);
  330. expect(projectData(null, {})).to.be.eq(null);
  331. });
  332. it('should project an array items', function () {
  333. const list = [{foo: 10, bar: 20, baz: 30}, {qux: 30}];
  334. const expectedList = [{foo: 10, baz: 30}, {qux: 30}];
  335. const res = projectData(list, {foo: true, bar: false});
  336. expect(res).to.be.eql(expectedList);
  337. });
  338. it('should project an array items in the strict mode', function () {
  339. const list = [{foo: 10, bar: 20, baz: 30}, {qux: 30}];
  340. const expectedList = [{foo: 10}, {}];
  341. const res = projectData(list, {foo: true, bar: false}, {strict: true});
  342. expect(res).to.be.eql(expectedList);
  343. });
  344. it('should exclude unknown properties when the strict mode is enabled', function () {
  345. const res = projectData(
  346. {foo: 10, bar: 20, baz: 30},
  347. {foo: true, bar: false},
  348. {strict: true},
  349. );
  350. expect(res).to.be.eql({foo: 10});
  351. });
  352. it('should include a property defined with an empty object in the strict mode', function () {
  353. const schema = {foo: {}, bar: {}};
  354. const data = {foo: 10, baz: 30};
  355. const res = projectData(data, schema, {strict: true});
  356. expect(res).to.be.eql({foo: 10});
  357. });
  358. it('should include a property with a nested schema in the strict mode', function () {
  359. const schema = {user: {schema: {id: true, name: false}}};
  360. const data = {user: {id: 1, name: 'John Doe'}, timestamp: 12345};
  361. const res = projectData(data, schema, {strict: true});
  362. expect(res).to.be.eql({user: {id: 1}});
  363. });
  364. it('should exclude a property when the "select" option is false in the strict mode', function () {
  365. const schema = {foo: {}, bar: {select: false}};
  366. const data = {foo: 10, bar: 20};
  367. const res = projectData(data, schema, {strict: true});
  368. expect(res).to.be.eql({foo: 10});
  369. });
  370. it('should include a property when the "select" option is true in the strict mode', function () {
  371. const schema = {foo: {}, bar: {select: true}};
  372. const data = {foo: 10, bar: 20};
  373. const res = projectData(data, schema, {strict: true});
  374. expect(res).to.be.eql({foo: 10, bar: 20});
  375. });
  376. it('should ignore prototype properties', function () {
  377. const data = Object.create({baz: 30});
  378. data.foo = 10;
  379. data.bar = 20;
  380. expect(data).to.be.eql({foo: 10, bar: 20, baz: 30});
  381. const res = projectData(data, {foo: true, bar: false});
  382. expect(res).to.be.eql({foo: 10});
  383. });
  384. it('should project the property by a boolean rule', function () {
  385. const res = projectData({foo: 10, bar: 20}, {foo: true, bar: false});
  386. expect(res).to.be.eql({foo: 10});
  387. });
  388. it('should project the property by the select option', function () {
  389. const res = projectData(
  390. {foo: 10, bar: 20},
  391. {foo: {select: true}, bar: {select: false}},
  392. );
  393. expect(res).to.be.eql({foo: 10});
  394. });
  395. it('should ignore scope options when no active scope is provided', function () {
  396. const schema = {
  397. foo: {select: true, scopes: {input: false}},
  398. bar: {select: false, scopes: {output: true}},
  399. };
  400. const res = projectData({foo: 10, bar: 20}, schema);
  401. expect(res).to.be.eql({foo: 10});
  402. });
  403. it('should project the active scope by the boolean rule', function () {
  404. const schema = {
  405. foo: {scopes: {input: true}},
  406. bar: {scopes: {input: false}},
  407. };
  408. const res = projectData({foo: 10, bar: 20}, schema, {scope: 'input'});
  409. expect(res).to.be.eql({foo: 10});
  410. });
  411. it('should project the active scope by the select option', function () {
  412. const schema = {
  413. foo: {scopes: {input: {select: true}}},
  414. bar: {scopes: {input: {select: false}}},
  415. };
  416. const res = projectData({foo: 10, bar: 20}, schema, {scope: 'input'});
  417. expect(res).to.be.eql({foo: 10});
  418. });
  419. it('should prioritize the scope rule over the general options', function () {
  420. const schema = {
  421. foo: {select: false, scopes: {input: true}},
  422. bar: {select: true, scopes: {input: false}},
  423. };
  424. const res = projectData({foo: 10, bar: 20}, schema, {scope: 'input'});
  425. expect(res).to.be.eql({foo: 10});
  426. });
  427. it('should ignore scope options not matched with the active scope', function () {
  428. const schema = {
  429. foo: {scopes: {input: true, output: false}},
  430. bar: {scopes: {input: false, output: true}},
  431. };
  432. const res = projectData({foo: 10, bar: 20}, schema, {scope: 'input'});
  433. expect(res).to.be.eql({foo: 10});
  434. });
  435. it('should include a property in the strict mode if no rule for the active scope is specified', function () {
  436. const schema = {
  437. foo: {scopes: {input: true}},
  438. bar: {scopes: {input: false}},
  439. baz: {scopes: {output: true}},
  440. };
  441. const data = {foo: 10, bar: 20, baz: 30, qux: 40};
  442. const res = projectData(data, schema, {strict: true, scope: 'input'});
  443. expect(res).to.be.eql({foo: 10, baz: 30});
  444. });
  445. it('should prioritize the scope options over the general options in the strict mode', function () {
  446. const schema = {
  447. foo: {select: false, scopes: {input: true}},
  448. bar: {select: false, scopes: {input: {select: true}}},
  449. };
  450. const data = {foo: 10, bar: 20, baz: 30};
  451. const res = projectData(data, schema, {strict: true, scope: 'input'});
  452. expect(res).to.be.eql({foo: 10, bar: 20});
  453. });
  454. it('should project the nested object by the given schema', function () {
  455. const schema = {
  456. foo: true,
  457. bar: {schema: {baz: true, qux: false}},
  458. };
  459. const data = {foo: 10, bar: {baz: 20, qux: 30, buz: 40}};
  460. const res = projectData(data, schema, {scope: 'input'});
  461. expect(res).to.be.eql({foo: 10, bar: {baz: 20, buz: 40}});
  462. });
  463. });