diff --git a/spec/v1/providers/database.spec.ts b/spec/v1/providers/database.spec.ts index 30d24ea3c..304c1d1fd 100644 --- a/spec/v1/providers/database.spec.ts +++ b/spec/v1/providers/database.spec.ts @@ -639,6 +639,10 @@ 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) @@ -646,45 +650,6 @@ 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', () => { @@ -711,37 +676,14 @@ describe('Database Functions', () => { }); it('should be false for a non-existent value', () => { - populate({ a: { b: 'c', nullChild: null } }); + populate({ a: { b: 'c' } }); 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', () => { @@ -770,17 +712,6 @@ 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', () => { @@ -820,51 +751,13 @@ describe('Database Functions', () => { describe('#numChildren()', () => { it('should be key count for objects', () => { - populate({ - a: 'b', - c: 'd', - nullChild: null, - emptyObjectChild: {}, - emptyArrayChild: [], - }); + populate({ a: 'b', c: 'd' }); 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; }); }); @@ -876,17 +769,9 @@ describe('Database Functions', () => { }); it('should return false if a child is missing', () => { - populate({ - a: 'b', - nullChild: null, - emptyObjectChild: {}, - emptyArrayChild: [], - }); + populate({ a: 'b' }); 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; }); }); @@ -916,21 +801,11 @@ describe('Database Functions', () => { describe('#toJSON(): Object', () => { it('should return the current value', () => { - populate({ - a: 'b', - nullChild: null, - emptyObjectChild: {}, - emptyArrayChild: [], - }); + populate({ a: 'b' }); expect(subject.toJSON()).to.deep.equal(subject.val()); }); it('should be stringifyable', () => { - populate({ - a: 'b', - nullChild: null, - emptyObjectChild: {}, - emptyArrayChild: [], - }); + populate({ a: 'b' }); expect(JSON.stringify(subject)).to.deep.equal('{"a":"b"}'); }); }); diff --git a/src/common/providers/database.ts b/src/common/providers/database.ts index f14ce8b1d..f1c41d617 100644 --- a/src/common/providers/database.ts +++ b/src/common/providers/database.ts @@ -21,13 +21,13 @@ // SOFTWARE. import * as firebase from 'firebase-admin'; -import { firebaseConfig } from '../../config'; +import * as _ from 'lodash'; import { joinPath, pathParts } from '../../utilities/path'; /** - * Interface representing a Firebase Realtime database data snapshot. + * Interface representing a Firebase Realtime Database data snapshot. */ -export class DataSnapshot implements firebase.database.DataSnapshot { +export class DataSnapshot { public instance: string; /** @hidden */ @@ -44,11 +44,10 @@ export class DataSnapshot implements firebase.database.DataSnapshot { constructor( data: any, - path?: string, // path is undefined for the database root + path?: string, // path will be undefined for the database root private app?: firebase.app.App, instance?: string ) { - const config = firebaseConfig(); if (app?.options?.databaseURL?.startsWith('http:')) { // In this case we're dealing with an emulator this.instance = app.options.databaseURL; @@ -57,13 +56,9 @@ export class DataSnapshot implements firebase.database.DataSnapshot { this.instance = instance; } else if (app) { this.instance = app.options.databaseURL; - } else if (config.databaseURL) { - this.instance = config.databaseURL; } else if (process.env.GCLOUD_PROJECT) { this.instance = - 'https://' + - process.env.GCLOUD_PROJECT + - '-default-rtdb.firebaseio.com'; + 'https://' + process.env.GCLOUD_PROJECT + '.firebaseio.com'; } this._path = path; @@ -72,7 +67,7 @@ export class DataSnapshot implements firebase.database.DataSnapshot { /** * Returns a [`Reference`](/docs/reference/admin/node/admin.database.Reference) - * to the database location where the triggering write occurred. Has + * to the Database location where the triggering write occurred. Has * full read and write access. */ get ref(): firebase.database.Reference { @@ -92,14 +87,13 @@ export class DataSnapshot implements firebase.database.DataSnapshot { /** * The key (last part of the path) of the location of this `DataSnapshot`. * - * The last token in a database location is considered its key. For example, + * The last token in a Database location is considered its key. For example, * "ada" is the key for the `/users/ada/` node. Accessing the key on any - * `DataSnapshot` returns the key for the location that generated it. - * However, accessing the key on the root URL of a database returns `null`. + * `DataSnapshot` will return the key for the location that generated it. + * However, accessing the key on the root URL of a Database will return `null`. */ - get key(): string | null { - const segments = pathParts(this._fullPath()); - const last = segments[segments.length - 1]; + get key(): string { + const last = _.last(pathParts(this._fullPath())); return !last || last === '' ? null : last; } @@ -111,25 +105,24 @@ export class DataSnapshot implements firebase.database.DataSnapshot { * return `null`, indicating that the `DataSnapshot` is empty (contains no * data). * - * @return The snapshot's contents as a JavaScript value (Object, + * @return The DataSnapshot's contents as a JavaScript value (Object, * Array, string, number, boolean, or `null`). */ val(): any { const parts = pathParts(this._childPath); - let source = this._data; - if (parts.length) { - for (const part of parts) { - source = source[part]; - } - } - const node = source ?? null; - + const source = this._data; + const node = _.cloneDeep( + parts.length ? _.get(source, parts, null) : source + ); return this._checkAndConvertToArray(node); } /** * Exports the entire contents of the `DataSnapshot` as a JavaScript object. * + * The `exportVal()` method is similar to `val()`, except priority information + * is included (if available), making it suitable for backing up your data. + * * @return The contents of the `DataSnapshot` as a JavaScript value * (Object, Array, string, number, boolean, or `null`). */ @@ -157,14 +150,7 @@ export class DataSnapshot implements firebase.database.DataSnapshot { * @return `true` if this `DataSnapshot` contains any data; otherwise, `false`. */ exists(): boolean { - const val = this.val(); - if (!val || val === null) { - return false; - } - if (typeof val === 'object' && Object.keys(val).length === 0) { - return false; - } - return true; + return !_.isNull(this.val()); } /** @@ -191,22 +177,25 @@ export class DataSnapshot implements firebase.database.DataSnapshot { * JavaScript object returned by `val()` is not guaranteed to match the ordering * on the server nor the ordering of `child_added` events. That is where * `forEach()` comes in handy. It guarantees the children of a `DataSnapshot` - * can be iterated in their query order. + * will be iterated in their query order. * * If no explicit `orderBy*()` method is used, results are returned * ordered by key (unless priorities are used, in which case, results are * returned by priority). * - * @param action A function that is called for each child `DataSnapshot`. + * @param action A function that will be called for each child `DataSnapshot`. * The callback can return `true` to cancel further enumeration. * * @return `true` if enumeration was canceled due to your callback * returning `true`. */ forEach(action: (a: DataSnapshot) => boolean | void): boolean { - const val = this.val() || {}; - if (typeof val === 'object') { - return Object.keys(val).some((key) => action(this.child(key)) === true); + const val = this.val(); + if (_.isPlainObject(val)) { + return _.some( + val, + (value, key: string) => action(this.child(key)) === true + ); } return false; } @@ -229,16 +218,14 @@ export class DataSnapshot implements firebase.database.DataSnapshot { * You can use `hasChildren()` to determine if a `DataSnapshot` has any * children. If it does, you can enumerate them using `forEach()`. If it * doesn't, then either this snapshot contains a primitive value (which can be - * retrieved with `val()`) or it is empty (in which case, `val()` returns + * retrieved with `val()`) or it is empty (in which case, `val()` will return * `null`). * * @return `true` if this snapshot has any children; else `false`. */ hasChildren(): boolean { const val = this.val(); - return ( - val !== null && typeof val === 'object' && Object.keys(val).length > 0 - ); + return _.isPlainObject(val) && _.keys(val).length > 0; } /** @@ -248,9 +235,7 @@ export class DataSnapshot implements firebase.database.DataSnapshot { */ numChildren(): number { const val = this.val(); - return val !== null && typeof val === 'object' - ? Object.keys(val).length - : 0; + return _.isPlainObject(val) ? Object.keys(val).length : 0; } /** @@ -282,12 +267,7 @@ export class DataSnapshot implements firebase.database.DataSnapshot { continue; } const childNode = node[key]; - const v = this._checkAndConvertToArray(childNode); - if (v === null) { - // Empty child node - continue; - } - obj[key] = v; + obj[key] = this._checkAndConvertToArray(childNode); numKeys++; const integerRegExp = /^(0|[1-9]\d*)$/; if (allIntegerKeys && integerRegExp.test(key)) { @@ -297,17 +277,12 @@ export class DataSnapshot implements firebase.database.DataSnapshot { } } - if (numKeys === 0) { - // Empty node - return null; - } - if (allIntegerKeys && maxKey < 2 * numKeys) { // convert to array. const array: any = []; - for (const key of Object.keys(obj)) { - array[key] = obj[key]; - } + _.forOwn(obj, (val, key) => { + array[key] = val; + }); return array; } @@ -333,6 +308,7 @@ export class DataSnapshot implements firebase.database.DataSnapshot { /** @hidden */ private _fullPath(): string { - return (this._path || '') + '/' + (this._childPath || ''); + const out = (this._path || '') + '/' + (this._childPath || ''); + return out; } }