project-data.spec.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  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 "schemaOrName" to be a non-empty string or an object', function () {
  6. const resolver = () => ({});
  7. const throwable = v => () => projectData(v, {}, {resolver});
  8. const error = s =>
  9. format(
  10. 'Projection schema must be an Object or a non-empty String ' +
  11. 'that represents a schema name, but %s was given.',
  12. s,
  13. );
  14. expect(throwable('')).to.throw(error('""'));
  15. expect(throwable(10)).to.throw(error('10'));
  16. expect(throwable(0)).to.throw(error('0'));
  17. expect(throwable(true)).to.throw(error('true'));
  18. expect(throwable(false)).to.throw(error('false'));
  19. expect(throwable([])).to.throw(error('Array'));
  20. expect(throwable(null)).to.throw(error('null'));
  21. expect(throwable(undefined)).to.throw(error('undefined'));
  22. throwable('mySchema')();
  23. throwable({})();
  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('str')).to.throw(error('"str"'));
  75. expect(throwable('')).to.throw(error('""'));
  76. expect(throwable(10)).to.throw(error('10'));
  77. expect(throwable(0)).to.throw(error('0'));
  78. expect(throwable(true)).to.throw(error('true'));
  79. expect(throwable(false)).to.throw(error('false'));
  80. expect(throwable([])).to.throw(error('Array'));
  81. expect(throwable({})).to.throw(error('Object'));
  82. expect(throwable(null)).to.throw(error('null'));
  83. throwable(() => undefined)();
  84. throwable(undefined)();
  85. });
  86. it('should throw an error if no resolver specified when a schema name is provided', function () {
  87. expect(() => projectData('mySchema', {})).to.throw(
  88. 'Unable to resolve the named schema "mySchema" without ' +
  89. 'a specified projection schema resolver.',
  90. );
  91. });
  92. it('should throw an error if the schema resolver returns an invalid value', function () {
  93. const throwable = v => () =>
  94. projectData('mySchema', {}, {resolver: () => v});
  95. const error = s =>
  96. format(
  97. 'Projection schema resolver must return an Object, but %s was given.',
  98. s,
  99. );
  100. expect(throwable('str')).to.throw(error('"str"'));
  101. expect(throwable('')).to.throw(error('""'));
  102. expect(throwable(10)).to.throw(error('10'));
  103. expect(throwable(0)).to.throw(error('0'));
  104. expect(throwable(true)).to.throw(error('true'));
  105. expect(throwable(false)).to.throw(error('false'));
  106. expect(throwable([])).to.throw(error('Array'));
  107. expect(throwable(null)).to.throw(error('null'));
  108. expect(throwable(undefined)).to.throw(error('undefined'));
  109. throwable({})();
  110. });
  111. it('should return a non-object and non-array data as is', function () {
  112. const fn = v => projectData({foo: true}, v);
  113. expect(fn('str')).to.be.eql('str');
  114. expect(fn('')).to.be.eql('');
  115. expect(fn(10)).to.be.eql(10);
  116. expect(fn(0)).to.be.eql(0);
  117. expect(fn(true)).to.be.eql(true);
  118. expect(fn(false)).to.be.eql(false);
  119. expect(fn(null)).to.be.eql(null);
  120. expect(fn(undefined)).to.be.eql(undefined);
  121. });
  122. it('should apply projection for each array element', function () {
  123. const schema = {foo: true, bar: false};
  124. const data = [
  125. {foo: 10, bar: 20, baz: 30},
  126. {foo: 40, bar: 50, baz: 60},
  127. ];
  128. const res = projectData(schema, data);
  129. expect(res).to.be.eql([
  130. {foo: 10, baz: 30},
  131. {foo: 40, baz: 60},
  132. ]);
  133. });
  134. it('should add fields without rules by default', function () {
  135. const res = projectData({}, {foo: 10, bar: 20});
  136. expect(res).to.be.eql({foo: 10, bar: 20});
  137. });
  138. it('should project fields by a boolean value', function () {
  139. const res = projectData({foo: true, bar: false}, {foo: 10, bar: 20});
  140. expect(res).to.be.eql({foo: 10});
  141. });
  142. it('should project fields by the select option', function () {
  143. const res = projectData(
  144. {foo: {select: true}, bar: {select: false}},
  145. {foo: 10, bar: 20},
  146. );
  147. expect(res).to.be.eql({foo: 10});
  148. });
  149. it('should ignore scope-related rules by default', function () {
  150. const res = projectData(
  151. {foo: {scopes: {input: false, output: false}}},
  152. {foo: 10},
  153. );
  154. expect(res).to.be.eql({foo: 10});
  155. });
  156. it('should create nested projection by the schema option', function () {
  157. const res = projectData(
  158. {foo: true, bar: false, qux: {schema: {abc: true, def: false}}},
  159. {foo: 10, bar: 20, qux: {abc: 30, def: 40}},
  160. );
  161. expect(res).to.be.eql({foo: 10, qux: {abc: 30}});
  162. });
  163. describe('schema name', function () {
  164. it('should pass the schema name to the schema resolver', function () {
  165. let invoked = 0;
  166. const resolver = name => {
  167. expect(name).to.be.eq('mySchema');
  168. invoked++;
  169. return {foo: true, bar: false};
  170. };
  171. const res = projectData('mySchema', {foo: 10, bar: 20}, {resolver});
  172. expect(res).to.be.eql({foo: 10});
  173. expect(invoked).to.be.eq(1);
  174. });
  175. it('should use the schema resolver in the nested schema', function () {
  176. let invoked = 0;
  177. const resolver = name => {
  178. expect(name).to.be.eq('mySchema');
  179. invoked++;
  180. return {baz: true, qux: false};
  181. };
  182. const res = projectData(
  183. {foo: true, bar: {schema: 'mySchema'}},
  184. {foo: 10, bar: {baz: 20, qux: 30}},
  185. {resolver},
  186. );
  187. expect(res).to.be.eql({foo: 10, bar: {baz: 20}});
  188. expect(invoked).to.be.eq(1);
  189. });
  190. });
  191. describe('strict mode', function () {
  192. it('should preserve fields not defined in the schema when the strict option is false', function () {
  193. const res = projectData({}, {foo: 10});
  194. expect(res).to.be.eql({foo: 10});
  195. });
  196. it('should remove fields without rules when the strict mode is enabled', function () {
  197. const res = projectData({}, {foo: 10}, {strict: true});
  198. expect(res).to.be.eql({});
  199. });
  200. it('should project fields by a boolean value', function () {
  201. const res = projectData(
  202. {foo: true, bar: false},
  203. {foo: 10, bar: 20},
  204. {strict: true},
  205. );
  206. expect(res).to.be.eql({foo: 10});
  207. });
  208. it('should project fields by the select option', function () {
  209. const res = projectData(
  210. {foo: {select: true}, bar: {select: false}},
  211. {foo: 10, bar: 20},
  212. {strict: true},
  213. );
  214. expect(res).to.be.eql({foo: 10});
  215. });
  216. it('should propagate the strict mode to nested schema', function () {
  217. const res = projectData(
  218. {foo: false, bar: {select: true, schema: {baz: true}}},
  219. {foo: 10, bar: {baz: 20, qux: 30}},
  220. {strict: true},
  221. );
  222. expect(res).to.be.eql({bar: {baz: 20}});
  223. });
  224. });
  225. describe('projection scope', function () {
  226. it('should apply scope-specific selection rule by a boolean value', function () {
  227. const schema = {
  228. foo: {
  229. select: false,
  230. scopes: {
  231. input: true,
  232. },
  233. },
  234. bar: true,
  235. };
  236. const data = {foo: 10, bar: 20};
  237. const res1 = projectData(schema, data);
  238. const res2 = projectData(schema, data, {scope: 'input'});
  239. expect(res1).to.be.eql({bar: 20});
  240. expect(res2).to.be.eql({foo: 10, bar: 20});
  241. });
  242. it('should apply scope-specific selection rule by the select option', function () {
  243. const schema = {
  244. foo: {
  245. select: false,
  246. scopes: {
  247. input: {select: true},
  248. },
  249. },
  250. bar: {select: true},
  251. };
  252. const data = {foo: 10, bar: 20};
  253. const res1 = projectData(schema, data);
  254. const res2 = projectData(schema, data, {scope: 'input'});
  255. expect(res1).to.be.eql({bar: 20});
  256. expect(res2).to.be.eql({foo: 10, bar: 20});
  257. });
  258. it('should fallback to general rule if scope rule is missing', function () {
  259. const schema = {
  260. foo: {
  261. select: true,
  262. scopes: {
  263. output: {select: false},
  264. },
  265. },
  266. };
  267. const data = {foo: 10};
  268. const res = projectData(schema, data, {scope: 'input'});
  269. expect(res).to.be.eql({foo: 10});
  270. });
  271. it('should fallback to the general rule if the scope options exists but lacks the select option', function () {
  272. const schema = {
  273. foo: {
  274. select: true,
  275. scopes: {
  276. input: {},
  277. },
  278. },
  279. bar: {
  280. select: false,
  281. scopes: {
  282. input: {},
  283. },
  284. },
  285. };
  286. const data = {foo: 10, bar: 20};
  287. const res = projectData(schema, data, {scope: 'input'});
  288. expect(res).to.be.eql({foo: 10});
  289. });
  290. });
  291. });