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 unknown properties 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 include a property defined with an empty object in the strict mode', function () { const schema = {foo: {}, bar: {}}; const data = {foo: 10, baz: 30}; const res = projectData(data, schema, {strict: true}); expect(res).to.be.eql({foo: 10}); }); it('should include a property with a nested schema in the strict mode', function () { const schema = {user: {schema: {id: true, name: false}}}; const data = {user: {id: 1, name: 'John Doe'}, timestamp: 12345}; const res = projectData(data, schema, {strict: true}); expect(res).to.be.eql({user: {id: 1}}); }); it('should exclude a property when the "select" option is false in the strict mode', function () { const schema = {foo: {}, bar: {select: false}}; const data = {foo: 10, bar: 20}; const res = projectData(data, schema, {strict: true}); expect(res).to.be.eql({foo: 10}); }); it('should include a property when the "select" option is true in the strict mode', function () { const schema = {foo: {}, bar: {select: true}}; const data = {foo: 10, bar: 20}; const res = projectData(data, schema, {strict: true}); expect(res).to.be.eql({foo: 10, bar: 20}); }); 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 include a property in the strict mode if no rule for the active scope is specified', 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, baz: 30}); }); 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}}); }); });