data-projector.spec.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. import {expect} from 'chai';
  2. import {format} from '@e22m4u/js-format';
  3. import {DataProjector} from './data-projector.js';
  4. import {ProjectionSchemaRegistry} from './projection-schema-registry.js';
  5. describe('DataProjector', function () {
  6. describe('defineSchema', function () {
  7. it('should require the name parameter to be a non-empty string', function () {
  8. const S = new DataProjector();
  9. const throwable = v => () => S.defineSchema(v, {});
  10. const error = s =>
  11. format('Schema name must be a non-empty String, but %s was given.', s);
  12. expect(throwable('')).to.throw(error('""'));
  13. expect(throwable(10)).to.throw(error('10'));
  14. expect(throwable(0)).to.throw(error('0'));
  15. expect(throwable(true)).to.throw(error('true'));
  16. expect(throwable(false)).to.throw(error('false'));
  17. expect(throwable([])).to.throw(error('Array'));
  18. expect(throwable({})).to.throw(error('Object'));
  19. expect(throwable(null)).to.throw(error('null'));
  20. expect(throwable(undefined)).to.throw(error('undefined'));
  21. throwable('mySchema')();
  22. });
  23. it('should require the schema parameter to be an object', function () {
  24. const S = new DataProjector();
  25. const throwable = v => () => S.defineSchema('mySchema', v);
  26. const error = s =>
  27. format('Projection schema must be an Object, but %s was given.', s);
  28. expect(throwable('str')).to.throw(error('"str"'));
  29. expect(throwable('')).to.throw(error('""'));
  30. expect(throwable(10)).to.throw(error('10'));
  31. expect(throwable(0)).to.throw(error('0'));
  32. expect(throwable(true)).to.throw(error('true'));
  33. expect(throwable(false)).to.throw(error('false'));
  34. expect(throwable([])).to.throw(error('Array'));
  35. expect(throwable(null)).to.throw(error('null'));
  36. expect(throwable(undefined)).to.throw(error('undefined'));
  37. throwable({})();
  38. });
  39. it('should throw an error if the name is already registered', function () {
  40. const S = new DataProjector();
  41. S.defineSchema('mySchema', {});
  42. const throwable = () => S.defineSchema('mySchema', {});
  43. expect(throwable).to.throw(
  44. 'Projection schema "mySchema" is already registered.',
  45. );
  46. });
  47. it('should register the given schema', function () {
  48. const S = new DataProjector();
  49. const registry = S.getService(ProjectionSchemaRegistry);
  50. const schema = {foo: true, bar: false};
  51. S.defineSchema('mySchema', schema);
  52. expect(registry.getSchema('mySchema')).to.be.eql(schema);
  53. });
  54. it('should return this', function () {
  55. const S = new DataProjector();
  56. const res = S.defineSchema('mySchema', {});
  57. expect(res).to.be.eq(S);
  58. });
  59. });
  60. describe('project', function () {
  61. it('should require the parameter "schemaOrName" to be a non-empty string or an object', function () {
  62. const S = new DataProjector();
  63. S.defineSchema('mySchema', {});
  64. const throwable = v => () => S.project(v, {});
  65. const error = s =>
  66. format(
  67. 'Projection schema must be an Object or a non-empty String ' +
  68. 'that represents a schema name, but %s was given.',
  69. s,
  70. );
  71. expect(throwable('')).to.throw(error('""'));
  72. expect(throwable(10)).to.throw(error('10'));
  73. expect(throwable(0)).to.throw(error('0'));
  74. expect(throwable(true)).to.throw(error('true'));
  75. expect(throwable(false)).to.throw(error('false'));
  76. expect(throwable([])).to.throw(error('Array'));
  77. expect(throwable(null)).to.throw(error('null'));
  78. expect(throwable(undefined)).to.throw(error('undefined'));
  79. throwable('mySchema')();
  80. throwable({})();
  81. });
  82. it('should require the parameter "options" to be an object', function () {
  83. const S = new DataProjector();
  84. const throwable = v => () => S.project({}, {}, v);
  85. const error = s =>
  86. format('Parameter "options" must be an Object, but %s was given.', s);
  87. expect(throwable('str')).to.throw(error('"str"'));
  88. expect(throwable('')).to.throw(error('""'));
  89. expect(throwable(10)).to.throw(error('10'));
  90. expect(throwable(0)).to.throw(error('0'));
  91. expect(throwable(true)).to.throw(error('true'));
  92. expect(throwable(false)).to.throw(error('false'));
  93. expect(throwable([])).to.throw(error('Array'));
  94. expect(throwable(null)).to.throw(error('null'));
  95. throwable({})();
  96. throwable(undefined)();
  97. });
  98. it('should require the option "strict" to be a boolean', function () {
  99. const S = new DataProjector();
  100. const throwable = v => () => S.project({}, {}, {strict: v});
  101. const error = s =>
  102. format('Option "strict" must be a Boolean, but %s was given.', s);
  103. expect(throwable('str')).to.throw(error('"str"'));
  104. expect(throwable('')).to.throw(error('""'));
  105. expect(throwable(10)).to.throw(error('10'));
  106. expect(throwable(0)).to.throw(error('0'));
  107. expect(throwable([])).to.throw(error('Array'));
  108. expect(throwable({})).to.throw(error('Object'));
  109. expect(throwable(null)).to.throw(error('null'));
  110. throwable(true)();
  111. throwable(false)();
  112. throwable(undefined)();
  113. });
  114. it('should require the option "scope" to be a non-empty string', function () {
  115. const S = new DataProjector();
  116. const throwable = v => () => S.project({}, {}, {scope: v});
  117. const error = s =>
  118. format(
  119. 'Option "scope" must be a non-empty String, but %s was given.',
  120. s,
  121. );
  122. expect(throwable('')).to.throw(error('""'));
  123. expect(throwable(10)).to.throw(error('10'));
  124. expect(throwable(0)).to.throw(error('0'));
  125. expect(throwable(true)).to.throw(error('true'));
  126. expect(throwable(false)).to.throw(error('false'));
  127. expect(throwable([])).to.throw(error('Array'));
  128. expect(throwable({})).to.throw(error('Object'));
  129. expect(throwable(null)).to.throw(error('null'));
  130. throwable('str')();
  131. throwable(undefined)();
  132. });
  133. it('should throw an error when the resolver option is provided', function () {
  134. const S = new DataProjector();
  135. // @ts-ignore
  136. const throwable = v => () => S.project({}, {}, {resolver: v});
  137. const error = 'Option "resolver" is not supported for the DataProjector.';
  138. expect(throwable('str')).to.throw(error);
  139. expect(throwable('')).to.throw(error);
  140. expect(throwable(10)).to.throw(error);
  141. expect(throwable(0)).to.throw(error);
  142. expect(throwable(true)).to.throw(error);
  143. expect(throwable(false)).to.throw(error);
  144. expect(throwable([])).to.throw(error);
  145. expect(throwable({})).to.throw(error);
  146. expect(throwable(null)).to.throw(error);
  147. throwable(undefined)();
  148. });
  149. it('should validate the given schema object', function () {
  150. const S = new DataProjector();
  151. // @ts-ignore
  152. const throwable = () => S.project({foo: 10}, {foo: 'bar'});
  153. expect(throwable).to.throw(
  154. 'Property options must be a Boolean or an Object, but 10 was given.',
  155. );
  156. });
  157. it('should throw an error if the schema name is not registered', function () {
  158. const S = new DataProjector();
  159. const throwable = () => S.project('unknown', {});
  160. expect(throwable).to.throw('Projection schema "unknown" is not found.');
  161. });
  162. it('should return non-object values as is', function () {
  163. const S = new DataProjector();
  164. const schema = {foo: {select: true}};
  165. expect(S.project(schema, 'str')).to.be.eq('str');
  166. expect(S.project(schema, '')).to.be.eq('');
  167. expect(S.project(schema, 10)).to.be.eq(10);
  168. expect(S.project(schema, 0)).to.be.eq(0);
  169. expect(S.project(schema, true)).to.be.eq(true);
  170. expect(S.project(schema, false)).to.be.eq(false);
  171. expect(S.project(schema, undefined)).to.be.eq(undefined);
  172. expect(S.project(schema, null)).to.be.eq(null);
  173. });
  174. it('should add properties without rules by default', function () {
  175. const S = new DataProjector();
  176. expect(S.project({}, {foo: 10, bar: 20})).to.be.eql({
  177. foo: 10,
  178. bar: 20,
  179. });
  180. });
  181. it('should project fields by a boolean value', function () {
  182. const S = new DataProjector();
  183. const schema = {foo: true, bar: false};
  184. const data = {foo: 10, bar: 20, baz: 30};
  185. expect(S.project(schema, data)).to.be.eql({foo: 10, baz: 30});
  186. });
  187. it('should project fields by the select option', function () {
  188. const S = new DataProjector();
  189. const schema = {foo: {select: true}, bar: {select: false}};
  190. const data = {foo: 10, bar: 20};
  191. expect(S.project(schema, data)).to.be.eql({foo: 10});
  192. });
  193. it('should project fields by the schema name', function () {
  194. const S = new DataProjector();
  195. S.defineSchema('user', {id: true, email: false});
  196. const data = {id: 1, email: 'test@example.com', name: 'John'};
  197. expect(S.project('user', data)).to.be.eql({id: 1, name: 'John'});
  198. });
  199. it('should apply projection to an array of items', function () {
  200. const S = new DataProjector();
  201. const schema = {id: {select: true}, secret: {select: false}};
  202. const data = [
  203. {id: 1, secret: 'A'},
  204. {id: 2, secret: 'B'},
  205. ];
  206. expect(S.project(schema, data)).to.be.eql([{id: 1}, {id: 2}]);
  207. });
  208. describe('strict mode', function () {
  209. it('should ignore properties not present in schema when strict mode is enabled', function () {
  210. const S = new DataProjector();
  211. const schema = {id: {select: true}};
  212. const data = {id: 1, other: 'value'};
  213. expect(S.project(schema, data, {strict: true})).to.be.eql({id: 1});
  214. });
  215. it('should project fields by a boolean value', function () {
  216. const S = new DataProjector();
  217. const schema = {foo: true, bar: false};
  218. const data = {foo: 1, bar: 2, baz: 3};
  219. expect(S.project(schema, data, {strict: true})).to.be.eql({foo: 1});
  220. });
  221. it('should default to hidden in strict mode if no rules are provided', function () {
  222. const S = new DataProjector();
  223. const schema = {id: {}};
  224. const data = {id: 1};
  225. expect(S.project(schema, data, {strict: true})).to.be.eql({});
  226. });
  227. it('should skip properties present in schema but missing in data', function () {
  228. const S = new DataProjector();
  229. const schema = {
  230. existing: {select: true},
  231. missing: {select: true},
  232. };
  233. const data = {existing: 1};
  234. expect(S.project(schema, data, {strict: true})).to.be.eql({
  235. existing: 1,
  236. });
  237. });
  238. });
  239. describe('projection scopes', function () {
  240. it('should apply scope-specific selection rules', function () {
  241. const S = new DataProjector();
  242. const schema = {
  243. foo: {
  244. select: false,
  245. scopes: {
  246. input: {select: true},
  247. },
  248. },
  249. bar: {select: true},
  250. };
  251. const data = {foo: 10, bar: 20};
  252. expect(S.project(schema, data)).to.be.eql({bar: 20});
  253. expect(S.project(schema, data, {scope: 'input'})).to.be.eql({
  254. foo: 10,
  255. bar: 20,
  256. });
  257. });
  258. it('should fallback to general rule if scope rule is missing', function () {
  259. const S = new DataProjector();
  260. const schema = {
  261. foo: {
  262. select: true,
  263. scopes: {
  264. output: {select: false},
  265. },
  266. },
  267. };
  268. const data = {foo: 10};
  269. expect(S.project(schema, data, {scope: 'input'})).to.be.eql({
  270. foo: 10,
  271. });
  272. });
  273. it('should fallback to the general rule if the scope options exists but lacks the select option', function () {
  274. const S = new DataProjector();
  275. const schema = {
  276. foo: {
  277. select: true,
  278. scopes: {
  279. input: {},
  280. },
  281. },
  282. bar: {
  283. select: false,
  284. scopes: {
  285. input: {},
  286. },
  287. },
  288. };
  289. const data = {foo: 10, bar: 20};
  290. const result = S.project(schema, data, {scope: 'input'});
  291. expect(result).to.be.eql({foo: 10});
  292. });
  293. });
  294. describe('nested schema', function () {
  295. it('should apply nested schema recursively', function () {
  296. const S = new DataProjector();
  297. const schema = {user: {schema: {password: false}}};
  298. const data = {user: {id: 1, password: '123'}};
  299. expect(S.project(schema, data)).to.be.eql({user: {id: 1}});
  300. });
  301. it('should apply nested registered schema by a name', function () {
  302. const S = new DataProjector();
  303. S.defineSchema('address', {zip: {select: false}});
  304. const schema = {location: {schema: 'address'}};
  305. const data = {location: {city: 'City', zip: '12345'}};
  306. expect(S.project(schema, data)).to.be.eql({location: {city: 'City'}});
  307. });
  308. it('should apply nested schema to array of objects', function () {
  309. const S = new DataProjector();
  310. const schema = {items: {schema: {hidden: false}}};
  311. const data = {
  312. items: [
  313. {id: 1, hidden: 'x'},
  314. {id: 2, hidden: 'y'},
  315. ],
  316. };
  317. expect(S.project(schema, data)).to.be.eql({items: [{id: 1}, {id: 2}]});
  318. });
  319. it('should handle null or undefined in nested data', function () {
  320. const S = new DataProjector();
  321. const schema = {nested: {schema: {foo: true}}};
  322. expect(S.project(schema, {nested: null})).to.be.eql({nested: null});
  323. expect(S.project(schema, {nested: undefined})).to.be.eql({
  324. nested: undefined,
  325. });
  326. });
  327. });
  328. });
  329. describe('projectInput', function () {
  330. it('should apply the given schema with the input scope', function () {
  331. const S = new DataProjector();
  332. const schema = {
  333. foo: {scopes: {input: true, output: false}},
  334. bar: {scopes: {input: false, output: true}},
  335. };
  336. const res = S.projectInput(schema, {foo: 10, bar: 20});
  337. expect(res).to.be.eql({foo: 10});
  338. });
  339. });
  340. describe('projectOutput', function () {
  341. it('should apply the given schema with the output scope', function () {
  342. const S = new DataProjector();
  343. const schema = {
  344. foo: {scopes: {input: true, output: false}},
  345. bar: {scopes: {input: false, output: true}},
  346. };
  347. const res = S.projectOutput(schema, {foo: 10, bar: 20});
  348. expect(res).to.be.eql({bar: 20});
  349. });
  350. });
  351. });