From 6d36c2788958bbab0b4dcb9d4334315bd13c4fa1 Mon Sep 17 00:00:00 2001 From: gianantoniopini <63844628+gianantoniopini@users.noreply.github.com> Date: Thu, 14 Jan 2021 14:00:07 +0100 Subject: [PATCH] Firebase Stub: simple implementation of data changes listener --- .../__tests__/gatsby-plugin-firebase.test.js | 53 ++++++++ .../constants/database.js | 6 + .../database/dataSnapshot.js | 4 +- .../database/database.js | 65 ++++++++- .../database/reference.js | 125 ++++++++++-------- 5 files changed, 193 insertions(+), 60 deletions(-) diff --git a/__mocks__/__tests__/gatsby-plugin-firebase.test.js b/__mocks__/__tests__/gatsby-plugin-firebase.test.js index 8ba2b89c..8d11abd5 100644 --- a/__mocks__/__tests__/gatsby-plugin-firebase.test.js +++ b/__mocks__/__tests__/gatsby-plugin-firebase.test.js @@ -280,5 +280,58 @@ describe('FirebaseStub', () => { expect(reference.orderByChildPath).toHaveLength(0); expect(reference.equalToValue).toHaveLength(0); }); + + it('triggers callback with resumes when creating new one', async () => { + const userUid = DatabaseConstants.user1.uid; + + let snapshotValue = null; + const callback = jest.fn((snapshot) => { + snapshotValue = snapshot.val(); + }); + + FirebaseStub.database() + .ref(DatabaseConstants.resumesPath) + .orderByChild('user') + .equalTo(userUid) + .on('value', callback); + + await waitFor(() => callback.mock.calls[0][0]); + + expect(callback.mock.calls).toHaveLength(1); + callback.mockClear(); + expect(snapshotValue).not.toBeNull(); + expect(Object.keys(snapshotValue)).toHaveLength(2); + Object.values(snapshotValue).forEach((resume) => + expect(resume.user).toEqual(userUid), + ); + snapshotValue = null; + + const existingResume = ( + await FirebaseStub.database() + .ref( + `${DatabaseConstants.resumesPath}/${DatabaseConstants.demoStateResume1Id}`, + ) + .once('value') + ).val(); + expect(existingResume).toBeTruthy(); + expect(existingResume.user).toEqual(userUid); + + const newResume = JSON.parse(JSON.stringify(existingResume)); + newResume.id = 'newre1'; + newResume.name = `Test Resume ${newResume.id}`; + + await FirebaseStub.database() + .ref(`${DatabaseConstants.resumesPath}/${newResume.id}`) + .set(newResume); + + await waitFor(() => callback.mock.calls[0][0]); + + expect(callback.mock.calls).toHaveLength(1); + callback.mockClear(); + expect(snapshotValue).not.toBeNull(); + expect(Object.keys(snapshotValue)).toHaveLength(3); + expect(snapshotValue[newResume.id]).toBeTruthy(); + expect(snapshotValue[newResume.id].id).toBe(newResume.id); + }); }); }); diff --git a/__mocks__/gatsby-plugin-firebase/constants/database.js b/__mocks__/gatsby-plugin-firebase/constants/database.js index 3d05331f..3c17834d 100644 --- a/__mocks__/gatsby-plugin-firebase/constants/database.js +++ b/__mocks__/gatsby-plugin-firebase/constants/database.js @@ -1,5 +1,7 @@ import AuthConstants from './auth'; +const valueEventType = 'value'; + const resumesPath = 'resumes'; const usersPath = 'users'; const connectedPath = '/.info/connected'; @@ -18,6 +20,10 @@ const user2 = { }; class Database { + static get valueEventType() { + return valueEventType; + } + static get resumesPath() { return resumesPath; } diff --git a/__mocks__/gatsby-plugin-firebase/database/dataSnapshot.js b/__mocks__/gatsby-plugin-firebase/database/dataSnapshot.js index 9fd4d453..f6b3c704 100644 --- a/__mocks__/gatsby-plugin-firebase/database/dataSnapshot.js +++ b/__mocks__/gatsby-plugin-firebase/database/dataSnapshot.js @@ -1,4 +1,6 @@ /* eslint-disable no-underscore-dangle */ +import DatabaseConstants from '../constants/database'; + class DataSnapshot { constructor(eventType, getData, value = undefined) { if (!eventType) { @@ -29,7 +31,7 @@ class DataSnapshot { } val() { - if (this.eventType === 'value') { + if (this.eventType === DatabaseConstants.valueEventType) { return typeof this.value !== 'undefined' ? this.value : this._getData(); } diff --git a/__mocks__/gatsby-plugin-firebase/database/database.js b/__mocks__/gatsby-plugin-firebase/database/database.js index 58cc0cb8..029071c2 100644 --- a/__mocks__/gatsby-plugin-firebase/database/database.js +++ b/__mocks__/gatsby-plugin-firebase/database/database.js @@ -39,6 +39,37 @@ class Database { return this._uuid; } + getData(dataPath) { + if (!dataPath) { + throw new Error('dataPath must be provided.'); + } + + const dataPathElements = dataPath.split('/'); + if (!(dataPathElements[0] in this._data)) { + return null; + } + + if (dataPathElements.length === 1) { + return this._data[dataPathElements[0]]; + } + + if (dataPathElements.length === 2) { + if (!(dataPathElements[1] in this._data[dataPathElements[0]])) { + return null; + } + + return this._data[dataPathElements[0]][dataPathElements[1]]; + } + + return null; + } + + getReference(referencePath) { + return referencePath in this._references + ? this._references[referencePath] + : null; + } + initializeData() { const resumes = {}; @@ -75,16 +106,46 @@ class Database { } ref(referencePath) { - const newRef = new Reference(referencePath, () => this._data); - const existingRef = this._references[newRef.path]; + const existingRef = this.getReference(referencePath); if (existingRef) { existingRef.initializeQueryParameters(); return existingRef; } + const newRef = new Reference( + referencePath, + (dataPath) => this.getData(dataPath), + (dataPath, value) => this.setData(dataPath, value), + (refPath) => this.getReference(refPath), + ); this._references[newRef.path] = newRef; return newRef; } + + setData(dataPath, value) { + if (!dataPath) { + throw new Error('dataPath must be provided.'); + } + + if (typeof value === 'undefined') { + throw new Error('value is undefined.'); + } + + const dataPathElements = dataPath.split('/'); + if (dataPathElements.length !== 2) { + return; + } + + if (!(dataPathElements[0] in this._data)) { + return; + } + + if (!dataPathElements[1]) { + return; + } + + this._data[dataPathElements[0]][dataPathElements[1]] = value; + } } export default Database; diff --git a/__mocks__/gatsby-plugin-firebase/database/reference.js b/__mocks__/gatsby-plugin-firebase/database/reference.js index b5a5466e..c14df0db 100644 --- a/__mocks__/gatsby-plugin-firebase/database/reference.js +++ b/__mocks__/gatsby-plugin-firebase/database/reference.js @@ -5,12 +5,10 @@ import { debounce } from 'lodash'; import DatabaseConstants from '../constants/database'; import DataSnapshot from './dataSnapshot'; -const rootPath = '.'; - class Reference { - constructor(path, getDatabaseData) { - if (typeof path === 'undefined' || path === null) { - this._path = rootPath; + constructor(path, getDatabaseData, setDatabaseData, getReference) { + if (!path) { + throw new Error('path must be provided.'); } else if (typeof path !== 'string') { throw new Error('path should be a string.'); } else { @@ -29,6 +27,24 @@ class Reference { this._getDatabaseData = getDatabaseData; + if (!setDatabaseData) { + throw new Error('setDatabaseData must be provided.'); + } else if (typeof getDatabaseData !== 'function') { + throw new Error('setDatabaseData should be a function.'); + } + + this._setDatabaseData = setDatabaseData; + + if (!getReference) { + throw new Error('getReference must be provided.'); + } else if (typeof getDatabaseData !== 'function') { + throw new Error('getReference should be a function.'); + } + + this._getReference = getReference; + + this._eventCallbacks = {}; + this.initializeQueryParameters(); } @@ -40,6 +56,10 @@ class Reference { return this._uuid; } + get eventCallbacks() { + return this._eventCallbacks; + } + get orderByChildPath() { return this._orderByChildPath; } @@ -48,55 +68,42 @@ class Reference { return this._equalToValue; } + debounceEventCallback(eventType) { + if (!(eventType in this.eventCallbacks)) { + return; + } + + let snapshot = new DataSnapshot(eventType, () => this.getData(), null); + + if (this.path === DatabaseConstants.connectedPath) { + snapshot = new DataSnapshot(eventType, () => this.getData(), true); + } else if (this.path === DatabaseConstants.resumesPath) { + snapshot = new DataSnapshot(eventType, () => this.getData()); + } + + const debouncedEventCallback = debounce( + this.eventCallbacks[eventType], + 100, + ); + debouncedEventCallback(snapshot); + } + getData() { - const databaseData = this._getDatabaseData(); + const databaseData = this._getDatabaseData(this.path); if (!databaseData) { return null; } - if (this.path === rootPath) { - return databaseData; - } - - let data = null; - if ( - this.path === DatabaseConstants.resumesPath || - this.path === DatabaseConstants.usersPath - ) { - data = this.path in databaseData ? databaseData[this.path] : null; - - if (data && this.orderByChildPath && this.equalToValue) { - return Object.fromEntries( - Object.entries(data).filter( - ([, value]) => value[this.orderByChildPath] === this.equalToValue, - ), - ); - } - - return data; - } - - if ( - this.path.startsWith(`${DatabaseConstants.resumesPath}/`) || - this.path.startsWith(`${DatabaseConstants.usersPath}/`) - ) { - const databaseLocationId = this.path.substring( - this.path.indexOf('/') + 1, + if (this.orderByChildPath && this.equalToValue) { + return Object.fromEntries( + Object.entries(databaseData).filter( + ([, value]) => value[this.orderByChildPath] === this.equalToValue, + ), ); - if (!databaseLocationId) { - throw new Error('Unknown database location id.'); - } - - const pathWithoutId = this.path.substring(0, this.path.indexOf('/')); - if (pathWithoutId in databaseData) { - return databaseLocationId in databaseData[pathWithoutId] - ? databaseData[pathWithoutId][databaseLocationId] - : null; - } } - return null; + return databaseData; } initializeQueryParameters() { @@ -109,20 +116,13 @@ class Reference { } on(eventType, callback) { - if (eventType !== 'value') { + if (eventType !== DatabaseConstants.valueEventType) { return; } - let snapshot = new DataSnapshot(eventType, () => this.getData(), null); + this._eventCallbacks[eventType] = callback; - if (this.path === DatabaseConstants.connectedPath) { - snapshot = new DataSnapshot(eventType, () => this.getData(), true); - } else if (this.path === DatabaseConstants.resumesPath) { - snapshot = new DataSnapshot(eventType, () => this.getData()); - } - - const debouncedCallback = debounce(callback, 100); - debouncedCallback(snapshot); + this.debounceEventCallback(eventType); } async once(eventType) { @@ -160,8 +160,19 @@ class Reference { throw new Error('value must be provided.'); } - const result = this !== null; - return Promise.resolve(result); + this._setDatabaseData(this.path, value); + + this.debounceEventCallback(DatabaseConstants.valueEventType); + + const pathElements = this.path.split('/'); + if (pathElements.length === 2) { + const parentReference = this._getReference(pathElements[0]); + if (parentReference) { + parentReference.debounceEventCallback(DatabaseConstants.valueEventType); + } + } + + return Promise.resolve(true); } }