| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- import {expect} from 'chai';
- import {format} from '@e22m4u/js-format';
- import {projectData} from './project-data.js';
- describe('projectData', function () {
- it('should require the "options" argument to be an object', function () {
- const throwable = v => () => projectData(10, {}, v);
- const error = s =>
- format('Projection options must be an Object, but %s was given.', s);
- expect(throwable('str')).to.throw(error('"str"'));
- expect(throwable('')).to.throw(error('""'));
- expect(throwable(10)).to.throw(error('10'));
- expect(throwable(0)).to.throw(error('0'));
- expect(throwable(true)).to.throw(error('true'));
- expect(throwable(false)).to.throw(error('false'));
- expect(throwable([])).to.throw(error('Array'));
- expect(throwable(null)).to.throw(error('null'));
- throwable({})();
- throwable(undefined)();
- });
- it('should require the "strict" option to be a boolean', function () {
- const throwable = v => () => projectData(10, {}, {strict: v});
- const error = s =>
- format(
- 'Projection option "strict" must be a Boolean, but %s was given.',
- s,
- );
- expect(throwable('str')).to.throw(error('"str"'));
- expect(throwable('')).to.throw(error('""'));
- expect(throwable(10)).to.throw(error('10'));
- expect(throwable(0)).to.throw(error('0'));
- expect(throwable([])).to.throw(error('Array'));
- expect(throwable({})).to.throw(error('Object'));
- expect(throwable(null)).to.throw(error('null'));
- throwable(true)();
- throwable(false)();
- throwable(undefined)();
- });
- it('should require the "scope" option to be a non-empty string', function () {
- const throwable = v => () => projectData(10, {}, {scope: v});
- const error = s =>
- format(
- 'Projection option "scope" must be a non-empty String, ' +
- 'but %s was given.',
- s,
- );
- expect(throwable('')).to.throw(error('""'));
- expect(throwable(10)).to.throw(error('10'));
- expect(throwable(0)).to.throw(error('0'));
- expect(throwable(true)).to.throw(error('true'));
- expect(throwable(false)).to.throw(error('false'));
- expect(throwable([])).to.throw(error('Array'));
- expect(throwable({})).to.throw(error('Object'));
- expect(throwable(null)).to.throw(error('null'));
- throwable('str')();
- throwable(undefined)();
- });
- it('should require the "nameResolver" option to be a function', function () {
- const throwable = v => () => projectData(10, {}, {nameResolver: v});
- const error = s =>
- format(
- 'Projection option "nameResolver" must be ' +
- 'a Function, but %s was given.',
- s,
- );
- expect(throwable('str')).to.throw(error('"str"'));
- expect(throwable('')).to.throw(error('""'));
- expect(throwable(10)).to.throw(error('10'));
- expect(throwable(0)).to.throw(error('0'));
- expect(throwable(true)).to.throw(error('true'));
- expect(throwable(false)).to.throw(error('false'));
- expect(throwable([])).to.throw(error('Array'));
- expect(throwable({})).to.throw(error('Object'));
- expect(throwable(null)).to.throw(error('null'));
- throwable(() => ({}))();
- throwable(undefined)();
- });
- it('should require the "factoryArgs" option to be an Array', function () {
- const throwable = v => () => projectData(10, {}, {factoryArgs: v});
- const error = s =>
- format(
- 'Projection option "factoryArgs" must be an Array, but %s was given.',
- s,
- );
- expect(throwable('str')).to.throw(error('"str"'));
- expect(throwable('')).to.throw(error('""'));
- expect(throwable(10)).to.throw(error('10'));
- expect(throwable(0)).to.throw(error('0'));
- expect(throwable(true)).to.throw(error('true'));
- expect(throwable(false)).to.throw(error('false'));
- expect(throwable({})).to.throw(error('Object'));
- expect(throwable(null)).to.throw(error('null'));
- throwable([1])();
- throwable([])();
- });
- it('should resolve a factory value as the schema object', function () {
- let invoked = 0;
- const factory = () => {
- invoked++;
- return {foo: true, bar: false};
- };
- const res = projectData({foo: 10, bar: 20}, factory);
- expect(res).to.be.eql({foo: 10});
- expect(invoked).to.be.eq(1);
- });
- it('should pass the "factoryArgs" option to the schema factory', function () {
- let invoked = 0;
- const factoryArgs = [1, 2, 3];
- const factory = (...args) => {
- invoked++;
- expect(args).to.be.eql(factoryArgs);
- return {foo: true, bar: false};
- };
- const res = projectData({foo: 10, bar: 20}, factory, {factoryArgs});
- expect(res).to.be.eql({foo: 10});
- expect(invoked).to.be.eq(1);
- });
- it('should pass the "factoryArgs" option to the schema factory in the nested schema', function () {
- let invoked = 0;
- const factoryArgs = [1, 2, 3];
- const factory = (...args) => {
- invoked++;
- expect(args).to.be.eql(factoryArgs);
- return {bar: true, baz: false};
- };
- const res = projectData(
- {foo: {bar: 10, baz: 20}},
- {foo: {schema: factory}},
- {factoryArgs},
- );
- expect(res).to.be.eql({foo: {bar: 10}});
- expect(invoked).to.be.eq(1);
- });
- it('should require a factory value to be an object or a non-empty string', function () {
- const throwable = v => () => projectData({}, () => v);
- const error = s =>
- format(
- 'Schema factory must return an Object ' +
- 'or a non-empty String, but %s was given.',
- s,
- );
- expect(throwable('')).to.throw(error('""'));
- expect(throwable(10)).to.throw(error('10'));
- expect(throwable(0)).to.throw(error('0'));
- expect(throwable(true)).to.throw(error('true'));
- expect(throwable(false)).to.throw(error('false'));
- expect(throwable([])).to.throw(error('Array'));
- expect(throwable(undefined)).to.throw(error('undefined'));
- expect(throwable(null)).to.throw(error('null'));
- expect(throwable(() => undefined)).to.throw(error('Function'));
- projectData({}, () => ({}));
- projectData({}, () => 'str', {nameResolver: () => ({})});
- });
- it('should resolve the schema name by the name resolver', function () {
- let invoked = 0;
- const nameResolver = name => {
- invoked++;
- expect(name).to.be.eql('mySchema');
- return {foo: true, bar: false};
- };
- const res = projectData({foo: 10, bar: 20}, 'mySchema', {nameResolver});
- expect(res).to.be.eql({foo: 10});
- expect(invoked).to.be.eq(1);
- });
- it('should require the "nameResolver" option when the schema name is provided', function () {
- const throwable = () => projectData({}, 'mySchema');
- expect(throwable).to.throw(
- 'Projection option "nameResolver" is required ' +
- 'to resolve "mySchema" name.',
- );
- });
- it('should require the name resolver to return an object', function () {
- const throwable = v => () =>
- projectData({}, 'mySchema', {nameResolver: () => v});
- const error = s =>
- format(
- 'Name resolver must return an Object, a Function ' +
- 'or a non-empty String, but %s was given.',
- s,
- );
- expect(throwable('')).to.throw(error('""'));
- expect(throwable(10)).to.throw(error('10'));
- expect(throwable(0)).to.throw(error('0'));
- expect(throwable(true)).to.throw(error('true'));
- expect(throwable(false)).to.throw(error('false'));
- expect(throwable([])).to.throw(error('Array'));
- expect(throwable(undefined)).to.throw(error('undefined'));
- expect(throwable(null)).to.throw(error('null'));
- throwable({})();
- });
- it('should resolve the schema name from the factory function', function () {
- let invoked = 0;
- const nameResolver = name => {
- invoked++;
- expect(name).to.be.eql('mySchema');
- return {foo: true, bar: false};
- };
- const res = projectData({foo: 10, bar: 20}, () => 'mySchema', {
- nameResolver,
- });
- expect(res).to.be.eql({foo: 10});
- expect(invoked).to.be.eq(1);
- });
- it('should resolve the schema name in the the nested object', function () {
- let invoked = 0;
- const nameResolver = name => {
- invoked++;
- if (name === 'schema1') {
- return {foo: true, bar: {schema: 'schema2'}};
- } else if (name === 'schema2') {
- return {baz: true, qux: false};
- }
- };
- const data = {foo: 10, bar: {baz: 20, qux: 30}};
- const res = projectData(data, 'schema1', {nameResolver});
- expect(res).to.be.eql({foo: 10, bar: {baz: 20}});
- expect(invoked).to.be.eq(2);
- });
- it('should resolve the schema object through the name chain', function () {
- let invoked = 0;
- const nameResolver = name => {
- invoked++;
- if (name === 'schema1') {
- return 'schema2';
- } else if (name === 'schema2') {
- return {foo: true, bar: false};
- }
- throw new Error('Invalid argument.');
- };
- const res = projectData({foo: 10, bar: 20}, 'schema1', {nameResolver});
- expect(res).to.be.eql({foo: 10});
- expect(invoked).to.be.eq(2);
- });
- it('should validate a resolved value from the name chain', function () {
- const nameResolver = v => name => {
- if (name === 'schema1') {
- return 'schema2';
- } else if (name === 'schema2') {
- return v;
- } else if (name === 'schema3') {
- return {};
- }
- throw new Error('Invalid argument.');
- };
- const throwable = v => () =>
- projectData({foo: 10, bar: 20}, 'schema1', {
- nameResolver: nameResolver(v),
- });
- const error = s =>
- format(
- 'Name resolver must return an Object, a Function ' +
- 'or a non-empty String, but %s was given.',
- s,
- );
- expect(throwable('')).to.throw(error('""'));
- expect(throwable(10)).to.throw(error('10'));
- expect(throwable(0)).to.throw(error('0'));
- expect(throwable(true)).to.throw(error('true'));
- expect(throwable(false)).to.throw(error('false'));
- expect(throwable([])).to.throw(error('Array'));
- expect(throwable(undefined)).to.throw(error('undefined'));
- expect(throwable(null)).to.throw(error('null'));
- throwable('schema3')();
- });
- it('should resolve schema names through the factory function', function () {
- let invoked = 0;
- const nameResolver = name => {
- invoked++;
- if (name === 'schema1') {
- return () => 'schema2';
- } else if (name === 'schema2') {
- return {foo: true, bar: false};
- }
- throw new Error('Invalid argument.');
- };
- const res = projectData({foo: 10, bar: 20}, 'schema1', {nameResolver});
- expect(res).to.be.eql({foo: 10});
- expect(invoked).to.be.eq(2);
- });
- it('should validate a return value of the factory resolved through the name chain', function () {
- const nameResolver = v => name => {
- if (name === 'schema1') {
- return 'schema2';
- } else if (name === 'schema2') {
- return () => v;
- } else if (name === 'schema3') {
- return {};
- }
- throw new Error('Invalid argument.');
- };
- const throwable = v => () =>
- projectData({foo: 10, bar: 20}, 'schema1', {
- nameResolver: nameResolver(v),
- });
- const error = s =>
- format(
- 'Schema factory must return an Object ' +
- 'or a non-empty String, but %s was given.',
- s,
- );
- expect(throwable('')).to.throw(error('""'));
- expect(throwable(10)).to.throw(error('10'));
- expect(throwable(0)).to.throw(error('0'));
- expect(throwable(true)).to.throw(error('true'));
- expect(throwable(false)).to.throw(error('false'));
- expect(throwable([])).to.throw(error('Array'));
- expect(throwable(undefined)).to.throw(error('undefined'));
- expect(throwable(null)).to.throw(error('null'));
- throwable('schema3')();
- });
- it('should validate the given schema in the shallow mode', function () {
- const schema1 = {foo: '?'};
- const schema2 = {foo: true, bar: {schema: {baz: '?'}}};
- expect(() => projectData({foo: 10}, schema1)).to.throw(
- 'Property options must be an Object or a Boolean, but "?" was given.',
- );
- const res = projectData({foo: 10}, schema2);
- expect(res).to.be.eql({foo: 10});
- expect(() => projectData({bar: {baz: 20}}, schema2)).to.throw(
- 'Property options must be an Object or a Boolean, but "?" was given.',
- );
- });
- it('should return primitive value as is', function () {
- expect(projectData('str', {})).to.be.eq('str');
- expect(projectData('', {})).to.be.eq('');
- expect(projectData(10, {})).to.be.eq(10);
- expect(projectData(0, {})).to.be.eq(0);
- expect(projectData(true, {})).to.be.eq(true);
- expect(projectData(false, {})).to.be.eq(false);
- expect(projectData(undefined, {})).to.be.eq(undefined);
- expect(projectData(null, {})).to.be.eq(null);
- });
- it('should project an array items', function () {
- const list = [{foo: 10, bar: 20, baz: 30}, {qux: 30}];
- const expectedList = [{foo: 10, baz: 30}, {qux: 30}];
- const res = projectData(list, {foo: true, bar: false});
- expect(res).to.be.eql(expectedList);
- });
- it('should project an array items in the strict mode', function () {
- const list = [{foo: 10, bar: 20, baz: 30}, {qux: 30}];
- const expectedList = [{foo: 10}, {}];
- const res = projectData(list, {foo: true, bar: false}, {strict: true});
- expect(res).to.be.eql(expectedList);
- });
- it('should exclude properties without rules when the strict mode is enabled', function () {
- const res = projectData(
- {foo: 10, bar: 20, baz: 30},
- {foo: true, bar: false},
- {strict: true},
- );
- expect(res).to.be.eql({foo: 10});
- });
- it('should ignore prototype properties', function () {
- const data = Object.create({baz: 30});
- data.foo = 10;
- data.bar = 20;
- expect(data).to.be.eql({foo: 10, bar: 20, baz: 30});
- const res = projectData(data, {foo: true, bar: false});
- expect(res).to.be.eql({foo: 10});
- });
- it('should project the property by a boolean rule', function () {
- const res = projectData({foo: 10, bar: 20}, {foo: true, bar: false});
- expect(res).to.be.eql({foo: 10});
- });
- it('should project the property by the select option', function () {
- const res = projectData(
- {foo: 10, bar: 20},
- {foo: {select: true}, bar: {select: false}},
- );
- expect(res).to.be.eql({foo: 10});
- });
- it('should ignore scope options when no active scope is provided', function () {
- const schema = {
- foo: {select: true, scopes: {input: false}},
- bar: {select: false, scopes: {output: true}},
- };
- const res = projectData({foo: 10, bar: 20}, schema);
- expect(res).to.be.eql({foo: 10});
- });
- it('should project the active scope by the boolean rule', function () {
- const schema = {
- foo: {scopes: {input: true}},
- bar: {scopes: {input: false}},
- };
- const res = projectData({foo: 10, bar: 20}, schema, {scope: 'input'});
- expect(res).to.be.eql({foo: 10});
- });
- it('should project the active scope by the select option', function () {
- const schema = {
- foo: {scopes: {input: {select: true}}},
- bar: {scopes: {input: {select: false}}},
- };
- const res = projectData({foo: 10, bar: 20}, schema, {scope: 'input'});
- expect(res).to.be.eql({foo: 10});
- });
- it('should prioritize the scope rule over the general options', function () {
- const schema = {
- foo: {select: false, scopes: {input: true}},
- bar: {select: true, scopes: {input: false}},
- };
- const res = projectData({foo: 10, bar: 20}, schema, {scope: 'input'});
- expect(res).to.be.eql({foo: 10});
- });
- it('should ignore scope options not matched with the active scope', function () {
- const schema = {
- foo: {scopes: {input: true, output: false}},
- bar: {scopes: {input: false, output: true}},
- };
- const res = projectData({foo: 10, bar: 20}, schema, {scope: 'input'});
- expect(res).to.be.eql({foo: 10});
- });
- it('should exclude properties without selection rule in the strict mode when the active scope is provided', function () {
- const schema = {
- foo: {scopes: {input: true}},
- bar: {scopes: {input: false}},
- baz: {scopes: {output: true}},
- };
- const data = {foo: 10, bar: 20, baz: 30, qux: 40};
- const res = projectData(data, schema, {strict: true, scope: 'input'});
- expect(res).to.be.eql({foo: 10});
- });
- it('should prioritize the scope options over the general options in the strict mode', function () {
- const schema = {
- foo: {select: false, scopes: {input: true}},
- bar: {select: false, scopes: {input: {select: true}}},
- };
- const data = {foo: 10, bar: 20, baz: 30};
- const res = projectData(data, schema, {strict: true, scope: 'input'});
- expect(res).to.be.eql({foo: 10, bar: 20});
- });
- it('should project the nested object by the given schema', function () {
- const schema = {
- foo: true,
- bar: {schema: {baz: true, qux: false}},
- };
- const data = {foo: 10, bar: {baz: 20, qux: 30, buz: 40}};
- const res = projectData(data, schema, {scope: 'input'});
- expect(res).to.be.eql({foo: 10, bar: {baz: 20, buz: 40}});
- });
- it('should exclude nested properties without rules in the strict mode', function () {
- const schema = {
- foo: true,
- bar: {select: true, schema: {baz: true, qux: false}},
- };
- const data = {foo: 10, bar: {baz: 20, qux: 30, buz: 40}};
- const res = projectData(data, schema, {strict: true, scope: 'input'});
- expect(res).to.be.eql({foo: 10, bar: {baz: 20}});
- });
- });
|