project-data.spec.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  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 resolver option to be a function', function () {
  58. const throwable = v => () => projectData({}, 10, {resolver: v});
  59. const error = s =>
  60. format(
  61. 'Projection option "resolver" must be a Function, but %s was given.',
  62. s,
  63. );
  64. expect(throwable('str')).to.throw(error('"str"'));
  65. expect(throwable('')).to.throw(error('""'));
  66. expect(throwable(10)).to.throw(error('10'));
  67. expect(throwable(0)).to.throw(error('0'));
  68. expect(throwable(true)).to.throw(error('true'));
  69. expect(throwable(false)).to.throw(error('false'));
  70. expect(throwable([])).to.throw(error('Array'));
  71. expect(throwable({})).to.throw(error('Object'));
  72. expect(throwable(null)).to.throw(error('null'));
  73. throwable(() => ({}))();
  74. throwable(undefined)();
  75. });
  76. it('should project with the schema factory', function () {
  77. let invoked = 0;
  78. const factory = () => {
  79. invoked++;
  80. return {foo: true, bar: false};
  81. };
  82. const res = projectData(factory, {foo: 10, bar: 20});
  83. expect(res).to.be.eql({foo: 10});
  84. expect(invoked).to.be.eq(1);
  85. });
  86. it('should require a factory value to be an object or a non-empty string', function () {
  87. const throwable = v => () => projectData(() => v, {});
  88. const error = s =>
  89. format(
  90. 'Schema factory must return an Object ' +
  91. 'or a non-empty String, but %s was given.',
  92. s,
  93. );
  94. expect(throwable('')).to.throw(error('""'));
  95. expect(throwable(10)).to.throw(error('10'));
  96. expect(throwable(0)).to.throw(error('0'));
  97. expect(throwable(true)).to.throw(error('true'));
  98. expect(throwable(false)).to.throw(error('false'));
  99. expect(throwable([])).to.throw(error('Array'));
  100. expect(throwable(undefined)).to.throw(error('undefined'));
  101. expect(throwable(null)).to.throw(error('null'));
  102. expect(throwable(() => undefined)).to.throw(error('Function'));
  103. projectData(() => ({}), {});
  104. projectData(() => 'str', {}, {resolver: () => ({})});
  105. });
  106. it('should resolve the schema name by the given resolver', function () {
  107. let invoked = 0;
  108. const resolver = name => {
  109. invoked++;
  110. expect(name).to.be.eql('mySchema');
  111. return {foo: true, bar: false};
  112. };
  113. const res = projectData('mySchema', {foo: 10, bar: 20}, {resolver});
  114. expect(res).to.be.eql({foo: 10});
  115. expect(invoked).to.be.eq(1);
  116. });
  117. it('should require the resolver option when the schema name is given', function () {
  118. const throwable = () => projectData('mySchema', {});
  119. expect(throwable).to.throw(
  120. 'Projection option "resolver" is required to resolve "mySchema" schema.',
  121. );
  122. });
  123. it('should require a resolver result to be an object', function () {
  124. const throwable = v => () =>
  125. projectData('mySchema', {}, {resolver: () => v});
  126. const error = s =>
  127. format('Schema resolver must return an Object, but %s was given.', s);
  128. expect(throwable('str')).to.throw(error('"str"'));
  129. expect(throwable('')).to.throw(error('""'));
  130. expect(throwable(10)).to.throw(error('10'));
  131. expect(throwable(0)).to.throw(error('0'));
  132. expect(throwable(true)).to.throw(error('true'));
  133. expect(throwable(false)).to.throw(error('false'));
  134. expect(throwable([])).to.throw(error('Array'));
  135. expect(throwable(undefined)).to.throw(error('undefined'));
  136. expect(throwable(null)).to.throw(error('null'));
  137. expect(throwable(() => undefined)).to.throw(error('Function'));
  138. throwable({})();
  139. });
  140. it('should resolver the schema name from the factory function', function () {
  141. let invoked = 0;
  142. const resolver = name => {
  143. invoked++;
  144. expect(name).to.be.eql('mySchema');
  145. return {foo: true, bar: false};
  146. };
  147. const res = projectData(() => 'mySchema', {foo: 10, bar: 20}, {resolver});
  148. expect(res).to.be.eql({foo: 10});
  149. expect(invoked).to.be.eq(1);
  150. });
  151. it('should resolve the named schema in the the nested object', function () {
  152. let invoked = 0;
  153. const resolver = name => {
  154. invoked++;
  155. if (name === 'schema1') {
  156. return {foo: true, bar: {schema: 'schema2'}};
  157. } else if (name === 'schema2') {
  158. return {baz: true, qux: false};
  159. }
  160. };
  161. const data = {foo: 10, bar: {baz: 20, qux: 30}};
  162. const res = projectData('schema1', data, {resolver});
  163. expect(res).to.be.eql({foo: 10, bar: {baz: 20}});
  164. expect(invoked).to.be.eq(2);
  165. });
  166. it('should validate the given schema in the shallow mode', function () {
  167. const schema1 = {foo: '?'};
  168. const schema2 = {foo: true, bar: {schema: {baz: '?'}}};
  169. expect(() => projectData(schema1, {foo: 10})).to.throw(
  170. 'Property options must be an Object or a Boolean, but "?" was given.',
  171. );
  172. const res = projectData(schema2, {foo: 10});
  173. expect(res).to.be.eql({foo: 10});
  174. expect(() => projectData(schema2, {bar: {baz: 20}})).to.throw(
  175. 'Property options must be an Object or a Boolean, but "?" was given.',
  176. );
  177. });
  178. it('should return primitive value as is', function () {
  179. expect(projectData({}, 'str')).to.be.eq('str');
  180. expect(projectData({}, '')).to.be.eq('');
  181. expect(projectData({}, 10)).to.be.eq(10);
  182. expect(projectData({}, 0)).to.be.eq(0);
  183. expect(projectData({}, true)).to.be.eq(true);
  184. expect(projectData({}, false)).to.be.eq(false);
  185. expect(projectData({}, undefined)).to.be.eq(undefined);
  186. expect(projectData({}, null)).to.be.eq(null);
  187. });
  188. it('should project an array items', function () {
  189. const list = [{foo: 10, bar: 20, baz: 30}, {qux: 30}];
  190. const expectedList = [{foo: 10, baz: 30}, {qux: 30}];
  191. const res = projectData({foo: true, bar: false}, list);
  192. expect(res).to.be.eql(expectedList);
  193. });
  194. it('should project an array items in the strict mode', function () {
  195. const list = [{foo: 10, bar: 20, baz: 30}, {qux: 30}];
  196. const expectedList = [{foo: 10}, {}];
  197. const res = projectData({foo: true, bar: false}, list, {strict: true});
  198. expect(res).to.be.eql(expectedList);
  199. });
  200. it('should exclude properties without rules when the strict mode is enabled', function () {
  201. const res = projectData(
  202. {foo: true, bar: false},
  203. {foo: 10, bar: 20, baz: 30},
  204. {strict: true},
  205. );
  206. expect(res).to.be.eql({foo: 10});
  207. });
  208. it('should ignore prototype properties', function () {
  209. const data = Object.create({baz: 30});
  210. data.foo = 10;
  211. data.bar = 20;
  212. expect(data).to.be.eql({foo: 10, bar: 20, baz: 30});
  213. const res = projectData({foo: true, bar: false}, data);
  214. expect(res).to.be.eql({foo: 10});
  215. });
  216. it('should project the property by a boolean rule', function () {
  217. const res = projectData({foo: true, bar: false}, {foo: 10, bar: 20});
  218. expect(res).to.be.eql({foo: 10});
  219. });
  220. it('should project the property by the select option', function () {
  221. const res = projectData(
  222. {foo: {select: true}, bar: {select: false}},
  223. {foo: 10, bar: 20},
  224. );
  225. expect(res).to.be.eql({foo: 10});
  226. });
  227. it('should ignore scope options when no active scope is provided', function () {
  228. const schema = {
  229. foo: {select: true, scopes: {input: false}},
  230. bar: {select: false, scopes: {output: true}},
  231. };
  232. const res = projectData(schema, {foo: 10, bar: 20});
  233. expect(res).to.be.eql({foo: 10});
  234. });
  235. it('should project the active scope by the boolean rule', function () {
  236. const schema = {
  237. foo: {scopes: {input: true}},
  238. bar: {scopes: {input: false}},
  239. };
  240. const res = projectData(schema, {foo: 10, bar: 20}, {scope: 'input'});
  241. expect(res).to.be.eql({foo: 10});
  242. });
  243. it('should project the active scope by the select option', function () {
  244. const schema = {
  245. foo: {scopes: {input: {select: true}}},
  246. bar: {scopes: {input: {select: false}}},
  247. };
  248. const res = projectData(schema, {foo: 10, bar: 20}, {scope: 'input'});
  249. expect(res).to.be.eql({foo: 10});
  250. });
  251. it('should prioritize the scope rule over the general options', function () {
  252. const schema = {
  253. foo: {select: false, scopes: {input: true}},
  254. bar: {select: true, scopes: {input: false}},
  255. };
  256. const res = projectData(schema, {foo: 10, bar: 20}, {scope: 'input'});
  257. expect(res).to.be.eql({foo: 10});
  258. });
  259. it('should ignore scope options not matched with the active scope', function () {
  260. const schema = {
  261. foo: {scopes: {input: true, output: false}},
  262. bar: {scopes: {input: false, output: true}},
  263. };
  264. const res = projectData(schema, {foo: 10, bar: 20}, {scope: 'input'});
  265. expect(res).to.be.eql({foo: 10});
  266. });
  267. it('should exclude properties without selection rule in the strict mode when the active scope is provided', function () {
  268. const schema = {
  269. foo: {scopes: {input: true}},
  270. bar: {scopes: {input: false}},
  271. baz: {scopes: {output: true}},
  272. };
  273. const data = {foo: 10, bar: 20, baz: 30, qux: 40};
  274. const res = projectData(schema, data, {strict: true, scope: 'input'});
  275. expect(res).to.be.eql({foo: 10});
  276. });
  277. it('should prioritize the scope options over the general options in the strict mode', function () {
  278. const schema = {
  279. foo: {select: false, scopes: {input: true}},
  280. bar: {select: false, scopes: {input: {select: true}}},
  281. };
  282. const data = {foo: 10, bar: 20, baz: 30};
  283. const res = projectData(schema, data, {strict: true, scope: 'input'});
  284. expect(res).to.be.eql({foo: 10, bar: 20});
  285. });
  286. it('should project the nested object by the given schema', function () {
  287. const schema = {
  288. foo: true,
  289. bar: {schema: {baz: true, qux: false}},
  290. };
  291. const data = {foo: 10, bar: {baz: 20, qux: 30, buz: 40}};
  292. const res = projectData(schema, data, {scope: 'input'});
  293. expect(res).to.be.eql({foo: 10, bar: {baz: 20, buz: 40}});
  294. });
  295. it('should exclude nested properties without rules in the strict mode', function () {
  296. const schema = {
  297. foo: true,
  298. bar: {select: true, schema: {baz: true, qux: false}},
  299. };
  300. const data = {foo: 10, bar: {baz: 20, qux: 30, buz: 40}};
  301. const res = projectData(schema, data, {strict: true, scope: 'input'});
  302. expect(res).to.be.eql({foo: 10, bar: {baz: 20}});
  303. });
  304. });