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 resolver option to be a function', function () { const throwable = v => () => projectData({}, 10, {resolver: v}); const error = s => format( 'Projection option "resolver" 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 project with the schema factory', function () { let invoked = 0; const factory = () => { invoked++; return {foo: true, bar: false}; }; const res = projectData(factory, {foo: 10, bar: 20}); expect(res).to.be.eql({foo: 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', {}, {resolver: () => ({})}); }); it('should resolve the schema name by the given resolver', function () { let invoked = 0; const resolver = name => { invoked++; expect(name).to.be.eql('mySchema'); return {foo: true, bar: false}; }; const res = projectData('mySchema', {foo: 10, bar: 20}, {resolver}); expect(res).to.be.eql({foo: 10}); expect(invoked).to.be.eq(1); }); it('should require the resolver option when the schema name is given', function () { const throwable = () => projectData('mySchema', {}); expect(throwable).to.throw( 'Projection option "resolver" is required to resolve "mySchema" schema.', ); }); it('should require a resolver result to be an object', function () { const throwable = v => () => projectData('mySchema', {}, {resolver: () => v}); const error = s => format('Schema resolver must return 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(undefined)).to.throw(error('undefined')); expect(throwable(null)).to.throw(error('null')); expect(throwable(() => undefined)).to.throw(error('Function')); throwable({})(); }); it('should resolver the schema name from the factory function', function () { let invoked = 0; const resolver = name => { invoked++; expect(name).to.be.eql('mySchema'); return {foo: true, bar: false}; }; const res = projectData(() => 'mySchema', {foo: 10, bar: 20}, {resolver}); expect(res).to.be.eql({foo: 10}); expect(invoked).to.be.eq(1); }); it('should resolve the named schema in the the nested object', function () { let invoked = 0; const resolver = 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('schema1', data, {resolver}); expect(res).to.be.eql({foo: 10, bar: {baz: 20}}); expect(invoked).to.be.eq(2); }); it('should validate the given schema in the shallow mode', function () { const schema1 = {foo: '?'}; const schema2 = {foo: true, bar: {schema: {baz: '?'}}}; expect(() => projectData(schema1, {foo: 10})).to.throw( 'Property options must be an Object or a Boolean, but "?" was given.', ); const res = projectData(schema2, {foo: 10}); expect(res).to.be.eql({foo: 10}); expect(() => projectData(schema2, {bar: {baz: 20}})).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({foo: true, bar: false}, list); 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({foo: true, bar: false}, list, {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: true, bar: false}, {foo: 10, bar: 20, baz: 30}, {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({foo: true, bar: false}, data); expect(res).to.be.eql({foo: 10}); }); it('should project the property by a boolean rule', function () { const res = projectData({foo: true, bar: false}, {foo: 10, bar: 20}); expect(res).to.be.eql({foo: 10}); }); it('should project the property by the select option', function () { const res = projectData( {foo: {select: true}, bar: {select: false}}, {foo: 10, bar: 20}, ); 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(schema, {foo: 10, bar: 20}); 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(schema, {foo: 10, bar: 20}, {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(schema, {foo: 10, bar: 20}, {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(schema, {foo: 10, bar: 20}, {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(schema, {foo: 10, bar: 20}, {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(schema, data, {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(schema, data, {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(schema, data, {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(schema, data, {strict: true, scope: 'input'}); expect(res).to.be.eql({foo: 10, bar: {baz: 20}}); }); });