diff --git a/spec/v1/providers/database.spec.ts b/spec/v1/providers/database.spec.ts index 27d5854c6..9c4130398 100644 --- a/spec/v1/providers/database.spec.ts +++ b/spec/v1/providers/database.spec.ts @@ -541,10 +541,6 @@ describe('Database Functions', () => { expect(subject.val()).to.equal(0); populate({ myKey: 0 }); expect(subject.val()).to.deep.equal({ myKey: 0 }); - - // Null values are still reported as null. - populate({ myKey: null }); - expect(subject.val()).to.deep.equal({ myKey: null }); }); // Regression test: .val() was returning array of nulls when there's a property called length (BUG#37683995) @@ -552,6 +548,45 @@ describe('Database Functions', () => { populate({ length: 3, foo: 'bar' }); expect(subject.val()).to.deep.equal({ length: 3, foo: 'bar' }); }); + + it('should deal with null-values appropriately', () => { + populate(null); + expect(subject.val()).to.be.null; + + populate({ myKey: null }); + expect(subject.val()).to.be.null; + }); + + it('should deal with empty object values appropriately', () => { + populate({}); + expect(subject.val()).to.be.null; + + populate({ myKey: {} }); + expect(subject.val()).to.be.null; + + populate({ myKey: { child: null } }); + expect(subject.val()).to.be.null; + }); + + it('should deal with empty array values appropriately', () => { + populate([]); + expect(subject.val()).to.be.null; + + populate({ myKey: [] }); + expect(subject.val()).to.be.null; + + populate({ myKey: [null] }); + expect(subject.val()).to.be.null; + + populate({ myKey: [{}] }); + expect(subject.val()).to.be.null; + + populate({ myKey: [{ myKey: null }] }); + expect(subject.val()).to.be.null; + + populate({ myKey: [{ myKey: {} }] }); + expect(subject.val()).to.be.null; + }); }); describe('#child(): DataSnapshot', () => { @@ -578,14 +613,37 @@ describe('Database Functions', () => { }); it('should be false for a non-existent value', () => { - populate({ a: { b: 'c' } }); + populate({ a: { b: 'c', nullChild: null } }); expect(subject.child('d').exists()).to.be.false; + expect(subject.child('nullChild').exists()).to.be.false; }); it('should be false for a value pathed beyond a leaf', () => { populate({ a: { b: 'c' } }); expect(subject.child('a/b/c').exists()).to.be.false; }); + + it('should be false for an empty object value', () => { + populate({ a: {} }); + expect(subject.child('a').exists()).to.be.false; + + populate({ a: { child: null } }); + expect(subject.child('a').exists()).to.be.false; + + populate({ a: { child: {} } }); + expect(subject.child('a').exists()).to.be.false; + }); + + it('should be false for an empty array value', () => { + populate({ a: [] }); + expect(subject.child('a').exists()).to.be.false; + + populate({ a: [null] }); + expect(subject.child('a').exists()).to.be.false; + + populate({ a: [{}] }); + expect(subject.child('a').exists()).to.be.false; + }); }); describe('#forEach(action: (a: DataSnapshot) => boolean): boolean', () => { @@ -614,6 +672,17 @@ describe('Database Functions', () => { expect(subject.forEach(counter)).to.equal(false); expect(count).to.eq(0); + + populate({ + a: 'foo', + nullChild: null, + emptyObjectChild: {}, + emptyArrayChild: [], + }); + count = 0; + + expect(subject.forEach(counter)).to.equal(false); + expect(count).to.eq(1); }); it('should cancel further enumeration if callback returns true', () => { @@ -653,13 +722,51 @@ describe('Database Functions', () => { describe('#numChildren()', () => { it('should be key count for objects', () => { - populate({ a: 'b', c: 'd' }); + populate({ + a: 'b', + c: 'd', + nullChild: null, + emptyObjectChild: {}, + emptyArrayChild: [], + }); expect(subject.numChildren()).to.eq(2); }); it('should be 0 for non-objects', () => { populate(23); expect(subject.numChildren()).to.eq(0); + + populate({ + nullChild: null, + emptyObjectChild: {}, + emptyArrayChild: [], + }); + expect(subject.numChildren()).to.eq(0); + }); + }); + + describe('#hasChildren()', () => { + it('should true for objects', () => { + populate({ + a: 'b', + c: 'd', + nullChild: null, + emptyObjectChild: {}, + emptyArrayChild: [], + }); + expect(subject.hasChildren()).to.be.true; + }); + + it('should be false for non-objects', () => { + populate(23); + expect(subject.hasChildren()).to.be.false; + + populate({ + nullChild: null, + emptyObjectChild: {}, + emptyArrayChild: [], + }); + expect(subject.hasChildren()).to.be.false; }); }); @@ -671,9 +778,17 @@ describe('Database Functions', () => { }); it('should return false if a child is missing', () => { - populate({ a: 'b' }); + populate({ + a: 'b', + nullChild: null, + emptyObjectChild: {}, + emptyArrayChild: [], + }); expect(subject.hasChild('c')).to.be.false; expect(subject.hasChild('a/b')).to.be.false; + expect(subject.hasChild('nullChild')).to.be.false; + expect(subject.hasChild('emptyObjectChild')).to.be.false; + expect(subject.hasChild('emptyArrayChild')).to.be.false; }); }); @@ -703,11 +818,21 @@ describe('Database Functions', () => { describe('#toJSON(): Object', () => { it('should return the current value', () => { - populate({ a: 'b' }); + populate({ + a: 'b', + nullChild: null, + emptyObjectChild: {}, + emptyArrayChild: [], + }); expect(subject.toJSON()).to.deep.equal(subject.val()); }); it('should be stringifyable', () => { - populate({ a: 'b' }); + populate({ + a: 'b', + nullChild: null, + emptyObjectChild: {}, + emptyArrayChild: [], + }); expect(JSON.stringify(subject)).to.deep.equal('{"a":"b"}'); }); }); diff --git a/src/v1/providers/database.ts b/src/v1/providers/database.ts index 372419169..868884d6d 100644 --- a/src/v1/providers/database.ts +++ b/src/v1/providers/database.ts @@ -471,7 +471,16 @@ export class DataSnapshot { * @return `true` if this `DataSnapshot` contains any data; otherwise, `false`. */ exists(): boolean { - return !_.isNull(this.val()); + const val = this.val(); + if (_.isNull(val)) { + // Null value + return false; + } + if (_.isObjectLike(val) && _.isEmpty(val)) { + // Empty object/array + return false; + } + return true; } /** @@ -512,7 +521,7 @@ export class DataSnapshot { */ forEach(action: (a: DataSnapshot) => boolean | void): boolean { const val = this.val(); - if (_.isPlainObject(val)) { + if (_.isObjectLike(val)) { return _.some( val, (value, key: string) => action(this.child(key)) === true @@ -546,7 +555,7 @@ export class DataSnapshot { */ hasChildren(): boolean { const val = this.val(); - return _.isPlainObject(val) && _.keys(val).length > 0; + return _.isObjectLike(val) && !_.isEmpty(val); } /** @@ -556,7 +565,7 @@ export class DataSnapshot { */ numChildren(): number { const val = this.val(); - return _.isPlainObject(val) ? Object.keys(val).length : 0; + return _.isObjectLike(val) ? _.keys(val).length : 0; } /** @@ -588,7 +597,12 @@ export class DataSnapshot { continue; } const childNode = node[key]; - obj[key] = this._checkAndConvertToArray(childNode); + const v = this._checkAndConvertToArray(childNode); + if (v === null) { + // Empty child node + continue; + } + obj[key] = v; numKeys++; const integerRegExp = /^(0|[1-9]\d*)$/; if (allIntegerKeys && integerRegExp.test(key)) { @@ -598,6 +612,11 @@ export class DataSnapshot { } } + if (numKeys === 0) { + // Empty node + return null; + } + if (allIntegerKeys && maxKey < 2 * numKeys) { // convert to array. const array: any = [];