From 646b338e7110a03b77cc5a60c5b897a3571d6593 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Fri, 1 Mar 2019 11:24:52 -0500 Subject: [PATCH 01/18] Add timestamp for webapi --- src/app/index.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/app/index.ts b/src/app/index.ts index 472b87b9..7acb14f1 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -52,14 +52,6 @@ export function firestore(app?: any): firebaseFirestoreModule.Firestore { let functionsCache; -export namespace database { - // This is just to follow the webs interface. On android and ios enable logging only accepts a boolean - // By default logging is set to Info. We will set to debug if true and none if false. - export function enableLogging(logger?: boolean | ((a: string) => any), persistent?: boolean): any { - firebase.enableLogging(logger, persistent); - } -} - export function functions(app?: any): firebaseFunctionsModule.Functions { if (app) { console.log("The 'app' param is ignored at the moment."); @@ -81,3 +73,13 @@ export function storage(app?: any): firebaseStorageModule.Storage { } return storageCache; } +export namespace database { + // This is just to follow the webs interface. On android and ios enable logging only accepts a boolean + // By default logging is set to Info. We will set to debug if true and none if false. + export function enableLogging(logger?: boolean | ((a: string) => any), persistent?: boolean): any { + firebase.enableLogging(logger, persistent); + } +} +export namespace database.ServerValue { + export let TIMESTAMP: Object = { ".sv": "timestamp" }; +} From d36d41ca6c2cf035b7a9b9ad954d4162571e984f Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Wed, 16 Jan 2019 13:46:12 -0500 Subject: [PATCH 02/18] [query] - Add webQueryInterface to implement database queries through webapi --- src/firebase.d.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/firebase.d.ts b/src/firebase.d.ts index 28bda5e2..34751bc0 100644 --- a/src/firebase.d.ts +++ b/src/firebase.d.ts @@ -546,6 +546,23 @@ export interface OnDisconnect { update(values: Object): Promise; } +// WebAPI Query +export interface Query { + on(eventType: string, callback: (a: any, b?: string) => any): Promise; + once(eventType: string): Promise; + off(eventType?: string, callback?: (a: DataSnapshot, b?: string | null) => any, context?: Object | null): void; + orderByChild(value: string): Query; + orderByKey(): Query; + orderByPriority(): Query; + orderByValue(): Query; + equalTo(value: string | number | boolean, key?: string): Query; + startAt(value: string | number | boolean, key?: string): Query; + endAt(value: string | number | boolean, key?: string): Query; + limitToFirst(value: number): Query; + limitToLast(value: number): Query; +} + +export function webQuery(path: string): Query; export interface DataSnapshot { key: string; From 9c193fcd3ee4c5f7e7a1d40203d75ec93577aa4a Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Wed, 16 Jan 2019 13:46:56 -0500 Subject: [PATCH 03/18] [query] - Rework firebaseWebApi queries to allow chaining of filters. Android implementation --- src/firebase.android.ts | 230 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 224 insertions(+), 6 deletions(-) diff --git a/src/firebase.android.ts b/src/firebase.android.ts index 4515c401..fbd77ae0 100755 --- a/src/firebase.android.ts +++ b/src/firebase.android.ts @@ -21,7 +21,15 @@ import { isDocumentReference } from "./firebase-common"; import * as firebaseFunctions from "./functions/functions"; +<<<<<<< HEAD import * as firebaseMessaging from "./messaging/messaging"; +======= +import * as appModule from "tns-core-modules/application"; +import { AndroidActivityResultEventData } from "tns-core-modules/application"; +import { ad as AndroidUtils } from "tns-core-modules/utils/utils"; +import lazy from "tns-core-modules/utils/lazy"; +import { firestore, User, OnDisconnect as OnDisconnectBase, DataSnapshot, Query as QueryBase } from "./firebase"; +>>>>>>> [query] - Rework firebaseWebApi queries to allow chaining of filters. Android implementation declare const com: any; const gmsAds = (com.google.android.gms).ads; @@ -1421,22 +1429,22 @@ firebase.keepInSync = (path, switchOn) => { }; firebase._addObservers = (to, updateCallback) => { - const listener = new com.google.firebase.database.ChildEventListener({ - onCancelled: databaseError => { + const listener: com.google.firebase.database.ChildEventListener = new com.google.firebase.database.ChildEventListener({ + onCancelled: (databaseError: com.google.firebase.database.DatabaseError) => { updateCallback({ error: databaseError.getMessage() }); }, - onChildAdded: (snapshot, previousChildKey) => { + onChildAdded: (snapshot: com.google.firebase.database.DataSnapshot, previousChildKey: string) => { updateCallback(firebase.getCallbackData('ChildAdded', snapshot)); }, - onChildRemoved: snapshot => { + onChildRemoved: (snapshot: com.google.firebase.database.DataSnapshot) => { updateCallback(firebase.getCallbackData('ChildRemoved', snapshot)); }, - onChildChanged: (snapshot, previousChildKey) => { + onChildChanged: (snapshot: com.google.firebase.database.DataSnapshot, previousChildKey: string) => { updateCallback(firebase.getCallbackData('ChildChanged', snapshot)); }, - onChildMoved: (snapshot, previousChildKey) => { + onChildMoved: (snapshot: com.google.firebase.database.DataSnapshot, previousChildKey: string) => { updateCallback(firebase.getCallbackData('ChildMoved', snapshot)); } }); @@ -1622,8 +1630,218 @@ firebase.update = (path, val) => { }); }; +<<<<<<< HEAD firebase.query = (updateCallback: (data: FBDataSingleEvent | FBErrorData) => void, path: string, options: QueryOptions): Promise => { return new Promise((resolve, reject) => { +======= +firebase.webQuery = (path: string): QueryBase => { + if (!firebase.initialized) { + console.error("Please run firebase.init() before firebase.query()"); + throw new Error("FirebaseApp is not initialized. Make sure you run firebase.init() first"); + } + const dbRef: com.google.firebase.database.DatabaseReference = firebase.instance.child(path); + return new Query(dbRef, path); +}; + +class Query implements QueryBase { + private query: com.google.firebase.database.Query; // Keep track of internal query state allowing us to chain filter/range/limit + private static eventListenerMap: Map> = new Map(); // A map to keep track all all the listeners attached for the specified eventType + + constructor(private dbRef: com.google.firebase.database.DatabaseReference, private path: string) { } + + on(eventType: string, callback: (a: any, b?: string) => any): Promise { + const onValueEvent = result => { + if (result.error) { + callback(result); // CAREFUL before we were calling result.error! + } else { + callback({ + key: result.key, + val: () => result.value, + exists: () => !!result.value + }); + } + }; + return new Promise((resolve, reject) => { + try { + if (firebase.instance === null) { + reject("Run init() first!"); + return; + } + const listener = this.createEventListener(eventType, onValueEvent); + if (!this.query) this.query = this.dbRef; // Need this when calling on() without doing a sort as this.query is undefined + + if (eventType === "value") { + this.query.addValueEventListener(listener as com.google.firebase.database.ValueEventListener); + } else if (eventType === "child_added" || eventType === "child_changed" || eventType === "child_removed" || eventType === "child_moved") { + this.query.addChildEventListener(listener as com.google.firebase.database.ChildEventListener); + } else { + reject("Invalid eventType. Use one of the following: 'value', 'child_added', 'child_changed', 'child_removed', or 'child_moved'"); + return; + } + // Add listener to our map which keeps track of eventType: child/value events + if (!Query.eventListenerMap.has(eventType)) { + Query.eventListenerMap.set(eventType, []); + } + Query.eventListenerMap.get(eventType).push(listener); // We need to keep track of the listeners to fully remove them when calling off + resolve(); + } catch (ex) { + console.log("Error in firebase.addValueEventListener: " + ex); + reject(ex); + } + }); + } + + once(eventType: string): Promise { + return new Promise((resolve, reject) => { + firebase.getValue(this.path).then(result => { + resolve({ + key: result.key, + val: () => result.value, + exists: () => !!result.value + }); + }); + }); + } + + off(eventType?: string): void { + // Remove all events if none specified + if (!eventType) { + Query.eventListenerMap.forEach((value: any[], key: string) => { + firebase.removeEventListeners(value, this.path); + }); + } else { // Remove only the event specified by the user + if (Query.eventListenerMap.get(eventType)) { + firebase.removeEventListeners(Query.eventListenerMap.get(eventType), this.path); + } + } + } + + orderByChild(value: string): Query { + if (this.query) { + throw new Error("You can't combine multiple orderBy calls!"); + } + this.query = this.dbRef.orderByChild(value); + return this; + } + + orderByKey(): Query { + if (this.query) { + throw new Error("You can't combine multiple orderBy calls!"); + } + this.query = this.dbRef.orderByKey(); + return this; + } + + orderByPriority(): Query { + if (this.query) { + throw new Error("You can't combine multiple orderBy calls!"); + } + this.query = this.dbRef.orderByPriority(); + return this; + } + + orderByValue(): Query { + if (this.query) { + throw new Error("You can't combine multiple orderBy calls!"); + } + this.query = this.dbRef.orderByValue(); + return this; + } + + // Unlike the order-by methods, you can combine multiple limit or range functions. + // For example, you can combine the startAt() and endAt() methods to limit the results to a specified range of values. + + equalTo(value: any, key?: string): Query { + if (key) { + this.query = this.query.equalTo(value, key); + } else { + this.query = this.query.equalTo(value); + } + return this; + } + + startAt(value: any, key?: string): Query { + if (key) { + this.query = this.query.startAt(value, key); + } else { + this.query = this.query.startAt(value); + } + return this; + } + + endAt(value: any, key?: string): Query { + if (key) { + this.query = this.query.endAt(value, key); + } else { + this.query = this.query.endAt(value); + } + return this; + } + + limitToFirst(value: number): Query { + this.query = this.query.limitToFirst(value); + return this; + } + + limitToLast(value: number): Query { + this.query = this.query.limitToLast(value); + return this; + } + /** + * Depending on the eventType, attach listeners at the specified Database location. Follow the WebApi which listens + * to specific events (Android is more generic value / child - which includes all events add, change, remove etc). + * Similar to firebase._addObserver but I do not want to listen for every event + */ + private createEventListener(eventType: string, callback): com.google.firebase.database.ValueEventListener | com.google.firebase.database.ChildEventListener { + let listener; + + if (eventType === "value") { + listener = new com.google.firebase.database.ValueEventListener({ + onDataChange: (snapshot: com.google.firebase.database.DataSnapshot) => { + callback(firebase.getCallbackData('ValueChanged', snapshot)); + }, + onCancelled: (databaseError: com.google.firebase.database.DatabaseError) => { + callback({ + error: databaseError.getMessage() + }); + } + }); + } else if (eventType === "child_added" || eventType === "child_changed" || eventType === "child_removed" || eventType === "child_moved") { + listener = new com.google.firebase.database.ChildEventListener({ + onCancelled: (databaseError: com.google.firebase.database.DatabaseError) => { + callback({ + error: databaseError.getMessage() + }); + }, + onChildAdded: (snapshot: com.google.firebase.database.DataSnapshot, previousChildKey: string) => { + if (eventType === "child_added") { + callback(firebase.getCallbackData(eventType, snapshot)); + } + }, + onChildRemoved: (snapshot: com.google.firebase.database.DataSnapshot) => { + if (eventType === "child_removed") { + callback(firebase.getCallbackData(eventType, snapshot)); + } + }, + onChildChanged: (snapshot: com.google.firebase.database.DataSnapshot, previousChildKey: string) => { + if (eventType === "child_changed") { + callback(firebase.getCallbackData(eventType, snapshot)); + } + }, + onChildMoved: (snapshot: com.google.firebase.database.DataSnapshot, previousChildKey: string) => { + if (eventType === "child_moved") { + callback(firebase.getCallbackData(eventType, snapshot)); + } + } + }); + } + return listener; + } +} + +firebase.query = (updateCallback, path, options) => { + return new Promise((resolve, reject) => { +>>>>>>> [query] - Rework firebaseWebApi queries to allow chaining of filters. Android implementation try { if (firebase.instance === null) { reject("Run init() first!"); From e8406dc6642c8e57f3e663ad280f74358c65b314 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Wed, 16 Jan 2019 13:50:26 -0500 Subject: [PATCH 04/18] [query] - Rework firebaseWebApi queries to allow chaining of filters. iOS implementation --- src/firebase.android.ts | 17 +--- src/firebase.ios.ts | 186 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+), 13 deletions(-) diff --git a/src/firebase.android.ts b/src/firebase.android.ts index fbd77ae0..11d382fe 100755 --- a/src/firebase.android.ts +++ b/src/firebase.android.ts @@ -11,6 +11,7 @@ import { GetAuthTokenOptions, GetAuthTokenResult, OnDisconnect as OnDisconnectBase, QueryOptions, + Query as QueryBase, User } from "./firebase"; import { @@ -21,15 +22,7 @@ import { isDocumentReference } from "./firebase-common"; import * as firebaseFunctions from "./functions/functions"; -<<<<<<< HEAD import * as firebaseMessaging from "./messaging/messaging"; -======= -import * as appModule from "tns-core-modules/application"; -import { AndroidActivityResultEventData } from "tns-core-modules/application"; -import { ad as AndroidUtils } from "tns-core-modules/utils/utils"; -import lazy from "tns-core-modules/utils/lazy"; -import { firestore, User, OnDisconnect as OnDisconnectBase, DataSnapshot, Query as QueryBase } from "./firebase"; ->>>>>>> [query] - Rework firebaseWebApi queries to allow chaining of filters. Android implementation declare const com: any; const gmsAds = (com.google.android.gms).ads; @@ -1629,11 +1622,10 @@ firebase.update = (path, val) => { } }); }; +firebase.query = (updateCallback, path, options) => { + return new Promise((resolve, reject) => { + -<<<<<<< HEAD -firebase.query = (updateCallback: (data: FBDataSingleEvent | FBErrorData) => void, path: string, options: QueryOptions): Promise => { - return new Promise((resolve, reject) => { -======= firebase.webQuery = (path: string): QueryBase => { if (!firebase.initialized) { console.error("Please run firebase.init() before firebase.query()"); @@ -1841,7 +1833,6 @@ class Query implements QueryBase { firebase.query = (updateCallback, path, options) => { return new Promise((resolve, reject) => { ->>>>>>> [query] - Rework firebaseWebApi queries to allow chaining of filters. Android implementation try { if (firebase.instance === null) { reject("Run init() first!"); diff --git a/src/firebase.ios.ts b/src/firebase.ios.ts index 8efb1495..283ec9b6 100755 --- a/src/firebase.ios.ts +++ b/src/firebase.ios.ts @@ -7,6 +7,7 @@ import { GetAuthTokenOptions, GetAuthTokenResult, OnDisconnect as OnDisconnectBase, QueryOptions, + Query as QueryBase, User } from "./firebase"; import { @@ -1384,6 +1385,191 @@ firebase.update = (path, val) => { } }); }; +firebase.webQuery = (path: string): QueryBase => { + if (!firebase.initialized) { + console.error("Please run firebase.init() before firebase.query()"); + throw new Error("FirebaseApp is not initialized. Make sure you run firebase.init() first"); + } + const dbRef: FIRDatabaseReference = FIRDatabase.database().reference().child(path); + return new Query(dbRef, path); +}; + +class Query implements QueryBase { + private query: FIRDatabaseQuery; // Keep track of internal query state allowing us to chain filter/range/limit + private static eventListenerMap: Map> = new Map(); // A map to keep track all all the listeners attached for the specified eventType + + constructor(private dbRef: FIRDatabaseReference, private path: string) { } + + on(eventType: string, callback: (a: any, b?: string) => any): Promise { + const onValueEvent = result => { + if (result.error) { + callback(result); // CAREFUL before we were calling result.error! + } else { + callback({ + key: result.key, + val: () => result.value, + exists: () => !!result.value + }); + } + }; + return new Promise((resolve, reject) => { + try { + if (eventType === "value" || eventType === "child_added" || eventType === "child_changed" + || eventType === "child_removed" || eventType === "child_moved") { + // This.query may not exist if we call on without any sorts + const firDatabaseHandle = this.query ? this.attachEventObserver(this.query, eventType, onValueEvent) : + this.attachEventObserver(this.dbRef, eventType, onValueEvent); + if (!Query.eventListenerMap.has(eventType)) { + Query.eventListenerMap.set(eventType, []); + } + Query.eventListenerMap.get(eventType).push(firDatabaseHandle); // We need to keep track of the listeners to fully remove them when calling off + } else { + reject("Invalid eventType. Use one of the following: 'value', 'child_added', 'child_changed', 'child_removed', or 'child_moved'"); + return; + } + resolve(); + } catch (ex) { + console.log("Error in firebase.addValueEventListener: " + ex); + reject(ex); + } + }); + } + + once(eventType: string): Promise { + return new Promise((resolve, reject) => { + firebase.getValue(this.path).then(result => { + resolve({ + key: result.key, + val: () => result.value, + exists: () => !!result.value + }); + }); + }); + } + + off(eventType?: string): void { + // Remove all events if none specified + if (!eventType) { + Query.eventListenerMap.forEach((value: any[], key: string) => { + firebase.removeEventListeners(value, this.path); + }); + } else { // Remove only the event specified by the user + if (Query.eventListenerMap.get(eventType)) { + firebase.removeEventListeners(Query.eventListenerMap.get(eventType), this.path); + } + } + } + + orderByChild(value: string): Query { + if (this.query) { + throw new Error("You can't combine multiple orderBy calls!"); + } + this.query = this.dbRef.queryOrderedByChild(value); + return this; + } + + orderByKey(): Query { + if (this.query) { + throw new Error("You can't combine multiple orderBy calls!"); + } + this.query = this.dbRef.queryOrderedByKey(); + return this; + } + + orderByPriority(): Query { + if (this.query) { + throw new Error("You can't combine multiple orderBy calls!"); + } + this.query = this.dbRef.queryOrderedByPriority(); + return this; + } + + orderByValue(): Query { + if (this.query) { + throw new Error("You can't combine multiple orderBy calls!"); + } + this.query = this.dbRef.queryOrderedByValue(); + return this; + } + + // Unlike the order-by methods, you can combine multiple limit or range functions. + // For example, you can combine the startAt() and endAt() methods to limit the results to a specified range of values. + + equalTo(value: any, key?: string): Query { + if (key) { + this.query = this.query.queryEqualToValueChildKey(value, key); + } else { + this.query = this.query.queryEqualToValue(value); + } + return this; + } + + startAt(value: any, key?: string): Query { + if (key) { + this.query = this.query.queryStartingAtValueChildKey(value, key); + } else { + this.query = this.query.queryStartingAtValue(value); + } + return this; + } + + endAt(value: any, key?: string): Query { + if (key) { + this.query = this.query.queryEndingAtValueChildKey(value, key); + } else { + this.query = this.query.queryEndingAtValue(value); + } + return this; + } + + limitToFirst(value: number): Query { + this.query = this.query.queryLimitedToFirst(value); + return this; + } + + limitToLast(value: number): Query { + this.query = this.query.queryLimitedToLast(value); + return this; + } + + /** + * Depending on the eventType, attach listeners at the specified Database location. Follow the WebApi which listens + * to specific events (Android is more generic value / child - which includes all events add, change, remove etc). + * Similar to firebase._addObserver but I do not want to listen for every event + */ + private attachEventObserver(dbRef: FIRDatabaseQuery | FIRDatabaseReference, eventType: string, callback): number { + let firEventType: FIRDataEventType; + switch (eventType) { + case "value": + firEventType = FIRDataEventType.Value; + break; + case "child_added": + firEventType = FIRDataEventType.ChildAdded; + break; + case "child_changed": + firEventType = FIRDataEventType.ChildChanged; + break; + case "child_removed": + firEventType = FIRDataEventType.ChildRemoved; + break; + case "child_moved": + firEventType = FIRDataEventType.ChildMoved; + break; + } + + const listener = dbRef.observeEventTypeWithBlockWithCancelBlock( + firEventType, + snapshot => { + callback(firebase.getCallbackData(eventType, snapshot)); + }, + firebaseError => { + callback({ + error: firebaseError.localizedDescription + }); + }); + return listener; + } +} firebase.query = (updateCallback: (data: FBDataSingleEvent) => void, path: string, options: QueryOptions): Promise => { return new Promise((resolve, reject) => { From b08f5340bbd8531eaa09557ce47759920b5db0af Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Wed, 16 Jan 2019 13:48:27 -0500 Subject: [PATCH 05/18] [query] - Rework database/index Query class to use the new query api --- src/app/database/index.ts | 179 ++++++++++++++------------------------ 1 file changed, 63 insertions(+), 116 deletions(-) diff --git a/src/app/database/index.ts b/src/app/database/index.ts index 892e431c..5ca7a94f 100644 --- a/src/app/database/index.ts +++ b/src/app/database/index.ts @@ -2,7 +2,7 @@ import * as firebase from "../../firebase"; import { AddEventListenerResult, FBData } from "../../firebase"; import { nextPushId } from "./util/NextPushId"; -export module database { +export namespace database { export interface DataSnapshot { // child(path: string): DataSnapshot; exists(): boolean; @@ -19,137 +19,85 @@ export module database { } export class Query { - private static registeredListeners: Map> = new Map(); - private static registeredCallbacks: Map any>> = new Map(); - protected path: string; - + private queryObject: firebase.Query; constructor(path: string) { this.path = path; + this.queryObject = firebase.webQuery(this.path); } - public on(eventType /* TODO use */: string, callback: (a: DataSnapshot | null, b?: string) => any, cancelCallbackOrContext?: Object | null, context?: Object | null): (a: DataSnapshot | null, b?: string) => any { - const onValueEvent = result => { - if (result.error) { - callback(result); - } else { - callback({ - key: result.key, - val: () => result.value, - exists: () => !!result.value - }); - } - }; - - firebase.addValueEventListener(onValueEvent, this.path).then( - (result: AddEventListenerResult) => { - if (!Query.registeredListeners.has(this.path)) { - Query.registeredListeners.set(this.path, []); - } - Query.registeredListeners.set(this.path, Query.registeredListeners.get(this.path).concat(result.listeners)); - }, - error => { - console.log("firebase.database().on error: " + error); - } - ); - - // remember the callbacks as we may need them for 'query' events - if (!Query.registeredCallbacks.has(this.path)) { - Query.registeredCallbacks.set(this.path, []); - } - Query.registeredCallbacks.get(this.path).push(callback); - - return null; + /** + * Listens for data changes at a particular location + * @param eventType One of the following strings: "value", "child_added", "child_changed", "child_removed", or "child_moved." + * @param callback A callback that fires when the specified event occurs. The callback will be passed a DataSnapshot. + * @returns The provided callback function is returned unmodified. + */ + public on(eventType: string, callback: (a: DataSnapshot | null, b?: string) => any, + cancelCallbackOrContext?: Object | null, context?: Object | null): (a: DataSnapshot | null, b?: string) => Function { + + /** + * Follow webApi which uses the eventType. Works right now but running into issue because we don't + * pass back a DataSnapshot with forEach / getChildren() implemented. So when an event fires the user + * gets the updated values, but it's not sorted. In Android and Web you would loop through the children + * (getChildren() and forEach()) which would be returned in the query order. + * See: https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot#forEach + */ + this.queryObject.on(eventType, callback).catch( error => { + console.log("firebase.database().on error: " + error); + }); + return callback; // According to firebase doc we just return the callback given } - public off(eventType? /* TODO use */: string, callback?: (a: DataSnapshot, b?: string | null) => any, context?: Object | null): any { - if (Query.registeredListeners.has(this.path)) { - firebase.removeEventListeners(Query.registeredListeners.get(this.path), this.path).then( - result => Query.registeredListeners.delete(this.path), - error => console.log("firebase.database().off error: " + error) - ); - } - Query.registeredCallbacks.delete(this.path); - return null; + /** + * Remove all callbacks for given eventType. If not eventType is given this + * detaches all callbacks previously attached with on(). + */ + public off(eventType?: string, callback?: (a: DataSnapshot, b?: string | null) => any, context?: Object | null): void { + // TODO: use callback rather than remove ALL listeners for a given eventType + this.queryObject.off(eventType); } - public once(eventType: string, successCallback?: (a: DataSnapshot, b?: string) => any, failureCallbackOrContext?: Object | null, context?: Object | null): Promise { - return new Promise((resolve, reject) => { - firebase.getValue(this.path).then(result => { - resolve({ - key: result.key, - val: () => result.value, - exists: () => !!result.value - }); - }); - }); + /** + * Listens for exactly one event of the specified event type, and then stops listening. + * @param eventType One of the following strings: "value", "child_added", "child_changed", "child_removed", or "child_moved." + */ + public once(eventType: string, successCallback?: (a: DataSnapshot, b?: string) => any, + failureCallbackOrContext?: Object | null, context?: Object | null): Promise { + return this.queryObject.once(eventType); } - private getOnValueEventHandler(): (data: FBData) => void { - return result => { - const callbacks = Query.registeredCallbacks.get(this.path); - callbacks && callbacks.map(callback => { - callback({ - key: result.key, - val: () => result.value, - exists: () => !!result.value - }); - }); - }; + /** + * Generates a new Query object ordered by the specified child key. Queries can only order + * by one key at a time. Calling orderByChild() multiple times on the same query is an error. + * @param child child key to order the results by + */ + public orderByChild(child: string): firebase.Query { + return this.queryObject.orderByChild(child); } - - public orderByChild(child: string): Query { - firebase.query( - this.getOnValueEventHandler(), - this.path, - { - orderBy: { - type: firebase.QueryOrderByType.CHILD, - value: child - } - } - ); - return this; + /** + * Generates a new Query object ordered by key. + * Sorts the results of a query by their (ascending) key values. + */ + public orderByKey(): firebase.Query { + return this.queryObject.orderByKey(); } - public orderByKey(): Query { - firebase.query( - this.getOnValueEventHandler(), - this.path, - { - orderBy: { - type: firebase.QueryOrderByType.KEY - } - } - ); - return this; + /** + * Generates a new Query object ordered by priority + */ + public orderByPriority(): firebase.Query { + return this.queryObject.orderByPriority(); } - public orderByPriority(): Query { - firebase.query( - this.getOnValueEventHandler(), - this.path, - { - orderBy: { - type: firebase.QueryOrderByType.PRIORITY - } - } - ); - return this; + /** + * Generates a new Query object ordered by value.If the children of a query are all scalar values + * (string, number, or boolean), you can order the results by their (ascending) values. + */ + public orderByValue(): firebase.Query { + return this.queryObject.orderByValue(); } - public orderByValue(): Query { - firebase.query( - this.getOnValueEventHandler(), - this.path, - { - orderBy: { - type: firebase.QueryOrderByType.VALUE - } - } - ); - return this; - } + // Didn't expose the filterby functions because they should be run after an orderby. (TODO: Limitby are exceptions....) } export class Reference extends Query { @@ -258,8 +206,7 @@ export module database { } } - export interface ThenableReference extends Reference /*, PromiseLike */ - { + export interface ThenableReference extends Reference /*, PromiseLike */ { } export class Database { From 8a180c97cfc31f64ab63846dc09d0fab30661282 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Wed, 16 Jan 2019 15:31:40 -0500 Subject: [PATCH 06/18] [readme] - Added documentation for webApi queries --- docs/DATABASE.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/DATABASE.md b/docs/DATABASE.md index 288c451a..7572f054 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -163,6 +163,9 @@ Firebase supports querying data and this plugin does too, since v2.0.0. Let's say we have the structure as defined at `setValue`, then use this query to retrieve the companies in country 'Bulgaria': +
+ Native API + ```js var onQueryEvent = function(result) { // note that the query returns 1 match at a time @@ -216,6 +219,40 @@ Let's say we have the structure as defined at `setValue`, then use this query to ``` For supported values of the orderBy/range/ranges/limit's `type` properties, take a look at the [`firebase-common.d.ts`](firebase-common.d.ts) TypeScript definitions in this repo. +
+
+ Web API + +Alternatively you can use the web api to query data. See [docs](https://firebase.google.com/docs/reference/js/firebase.database.Query) for more examples and the full api + +Some key notes: + +`off("eventType")` will remove all listeners for "eventType" at the given path. So you do not need to call `off()` +the same number of times you call `on()`. Listeners for all eventTypes will be removed if no eventType is provided. + +Filters (equalTo, starAt, endAt, LimitBy, etc) are only usable after you chain it with a sort. (While Firebase exposes these without doing +a sort, your callback is never called). Think about it, if you apply equalTo without an orderBy what are you checking key, value, priority ??? + +DO NOT try to apply more than one orderBy to the same query as this will throw (follows the api) +```typescript + const bad = firebaseWebApi.database().ref(path).orderByKey(); + bad.orderByValue(); // <------ will throw here! + + // However you could do the following: + firebaseWebApi.database().ref("/companies").orderByKey() + .equalTo("Google") + .on("value", onQueryEvent); + + firebaseWebApi.database().ref("/companies").orderByValue() + .startAt(1999) + .on("child_added", onQueryEvent); + + firebaseWebApi.database().ref("/companies").off("value"); + + // You can also do the following + firebase.webQuery("/companies").orderByKey().on("value", onQueryEvent); +``` +
### update Changes the values of the keys specified in the dictionary without overwriting other keys at this location. From b7014ae43f6be7a34fc643ab00ba4c551c29446e Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Tue, 5 Mar 2019 13:28:07 -0500 Subject: [PATCH 07/18] [query] - Update web query class to have all filter operations to be compatible with webapi --- src/app/database/index.ts | 60 ++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/src/app/database/index.ts b/src/app/database/index.ts index 5ca7a94f..527b2c4f 100644 --- a/src/app/database/index.ts +++ b/src/app/database/index.ts @@ -26,23 +26,16 @@ export namespace database { this.queryObject = firebase.webQuery(this.path); } - /** - * Listens for data changes at a particular location - * @param eventType One of the following strings: "value", "child_added", "child_changed", "child_removed", or "child_moved." - * @param callback A callback that fires when the specified event occurs. The callback will be passed a DataSnapshot. - * @returns The provided callback function is returned unmodified. - */ + /** + * Listens for data changes at a particular location + * @param eventType One of the following strings: "value", "child_added", "child_changed", "child_removed", or "child_moved." + * @param callback A callback that fires when the specified event occurs. The callback will be passed a DataSnapshot. + * @returns The provided callback function is returned unmodified. + */ public on(eventType: string, callback: (a: DataSnapshot | null, b?: string) => any, cancelCallbackOrContext?: Object | null, context?: Object | null): (a: DataSnapshot | null, b?: string) => Function { - /** - * Follow webApi which uses the eventType. Works right now but running into issue because we don't - * pass back a DataSnapshot with forEach / getChildren() implemented. So when an event fires the user - * gets the updated values, but it's not sorted. In Android and Web you would loop through the children - * (getChildren() and forEach()) which would be returned in the query order. - * See: https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot#forEach - */ - this.queryObject.on(eventType, callback).catch( error => { + this.queryObject.on(eventType, callback).catch(error => { console.log("firebase.database().on error: " + error); }); return callback; // According to firebase doc we just return the callback given @@ -63,7 +56,7 @@ export namespace database { */ public once(eventType: string, successCallback?: (a: DataSnapshot, b?: string) => any, failureCallbackOrContext?: Object | null, context?: Object | null): Promise { - return this.queryObject.once(eventType); + return this.queryObject.once(eventType); } /** @@ -97,7 +90,42 @@ export namespace database { return this.queryObject.orderByValue(); } - // Didn't expose the filterby functions because they should be run after an orderby. (TODO: Limitby are exceptions....) + /** + * Creates a Query with the specified starting point. The value to start at should match the type + * passed to orderBy(). If using orderByKey(), the value must be a string + */ + public startAt(value: number | string | boolean | null): firebase.Query { + return this.queryObject.startAt(value); + } + + /** + * Creates a Query with the specified ending point. The value to start at should match the type + * passed to orderBy(). If using orderByKey(), the value must be a string. + */ + public endAt(value: any, key?: string): firebase.Query { + return this.queryObject.endAt(value, key); + } + + /** + * Generate a new Query limited to the first specific number of children. + */ + public limitToFirst(value: number): firebase.Query { + return this.queryObject.limitToFirst(value); + } + + /** + * Generate a new Query limited to the last specific number of children. + */ + public limitToLast(value: number): firebase.Query { + return this.queryObject.limitToLast(value); + } + + /** + * Creates a Query that includes children that match the specified value. + */ + public equalTo(value: any, key?: string): firebase.Query { + return this.queryObject.equalTo(value, key); + } } export class Reference extends Query { From f7d813d18b30ee0d2c8f4290454c19554f4f39c7 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Tue, 5 Mar 2019 13:29:34 -0500 Subject: [PATCH 08/18] Type query.once() to return a promise with datasnapshot --- src/firebase.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/firebase.d.ts b/src/firebase.d.ts index 34751bc0..d2e672b8 100644 --- a/src/firebase.d.ts +++ b/src/firebase.d.ts @@ -549,7 +549,7 @@ export interface OnDisconnect { // WebAPI Query export interface Query { on(eventType: string, callback: (a: any, b?: string) => any): Promise; - once(eventType: string): Promise; + once(eventType: string): Promise; off(eventType?: string, callback?: (a: DataSnapshot, b?: string | null) => any, context?: Object | null): void; orderByChild(value: string): Query; orderByKey(): Query; From ebf8ff4f169aa5ca0601459487d35972e6583213 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Tue, 5 Mar 2019 13:32:59 -0500 Subject: [PATCH 09/18] [query][android] - Make webapi queries return a datasnapshot that follows the webapi --- src/firebase.android.ts | 87 ++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 45 deletions(-) diff --git a/src/firebase.android.ts b/src/firebase.android.ts index 11d382fe..b135b227 100755 --- a/src/firebase.android.ts +++ b/src/firebase.android.ts @@ -1503,10 +1503,10 @@ firebase.getValue = path => { } const listener = new com.google.firebase.database.ValueEventListener({ - onDataChange: snapshot => { + onDataChange: (snapshot: com.google.firebase.database.DataSnapshot) => { resolve(firebase.getCallbackData('ValueChanged', snapshot)); }, - onCancelled: databaseError => { + onCancelled: (databaseError: com.google.firebase.database.DatabaseError) => { reject(databaseError.getMessage()); } }); @@ -1622,9 +1622,6 @@ firebase.update = (path, val) => { } }); }; -firebase.query = (updateCallback, path, options) => { - return new Promise((resolve, reject) => { - firebase.webQuery = (path: string): QueryBase => { if (!firebase.initialized) { @@ -1639,20 +1636,15 @@ class Query implements QueryBase { private query: com.google.firebase.database.Query; // Keep track of internal query state allowing us to chain filter/range/limit private static eventListenerMap: Map> = new Map(); // A map to keep track all all the listeners attached for the specified eventType - constructor(private dbRef: com.google.firebase.database.DatabaseReference, private path: string) { } + constructor(private dbRef: com.google.firebase.database.DatabaseReference, private path: string) { + this.query = this.dbRef; + } on(eventType: string, callback: (a: any, b?: string) => any): Promise { const onValueEvent = result => { - if (result.error) { - callback(result); // CAREFUL before we were calling result.error! - } else { - callback({ - key: result.key, - val: () => result.value, - exists: () => !!result.value - }); - } + callback(result); }; + return new Promise((resolve, reject) => { try { if (firebase.instance === null) { @@ -1660,13 +1652,15 @@ class Query implements QueryBase { return; } const listener = this.createEventListener(eventType, onValueEvent); - if (!this.query) this.query = this.dbRef; // Need this when calling on() without doing a sort as this.query is undefined if (eventType === "value") { this.query.addValueEventListener(listener as com.google.firebase.database.ValueEventListener); } else if (eventType === "child_added" || eventType === "child_changed" || eventType === "child_removed" || eventType === "child_moved") { this.query.addChildEventListener(listener as com.google.firebase.database.ChildEventListener); } else { + callback({ + error: "Invalid eventType. Use one of the following: 'value', 'child_added', 'child_changed', 'child_removed', or 'child_moved'" + }); reject("Invalid eventType. Use one of the following: 'value', 'child_added', 'child_changed', 'child_removed', or 'child_moved'"); return; } @@ -1677,21 +1671,36 @@ class Query implements QueryBase { Query.eventListenerMap.get(eventType).push(listener); // We need to keep track of the listeners to fully remove them when calling off resolve(); } catch (ex) { - console.log("Error in firebase.addValueEventListener: " + ex); + console.log("Error in firebase.on: " + ex); reject(ex); } }); } - once(eventType: string): Promise { + once(eventType: string): Promise { return new Promise((resolve, reject) => { - firebase.getValue(this.path).then(result => { - resolve({ - key: result.key, - val: () => result.value, - exists: () => !!result.value + try { + if (firebase.instance === null) { + reject("Run init() first!"); + return; + } + const listener = new com.google.firebase.database.ValueEventListener({ + onDataChange: (snapshot: com.google.firebase.database.DataSnapshot) => { + resolve(nativeSnapshotToWebSnapshot(snapshot)); + }, + onCancelled: (databaseError: com.google.firebase.database.DatabaseError) => { + reject({ + error: databaseError.getMessage() + }); + } }); - }); + // Kind of akward since Android only has single listener for the value event type... + firebase.instance.child(this.path).addListenerForSingleValueEvent(listener); + } + catch (ex) { + console.log("Error in firebase.once: " + ex); + reject(ex); + } }); } @@ -1709,34 +1718,22 @@ class Query implements QueryBase { } orderByChild(value: string): Query { - if (this.query) { - throw new Error("You can't combine multiple orderBy calls!"); - } - this.query = this.dbRef.orderByChild(value); + this.query = this.query.orderByChild(value); return this; } orderByKey(): Query { - if (this.query) { - throw new Error("You can't combine multiple orderBy calls!"); - } - this.query = this.dbRef.orderByKey(); + this.query = this.query.orderByKey(); return this; } orderByPriority(): Query { - if (this.query) { - throw new Error("You can't combine multiple orderBy calls!"); - } - this.query = this.dbRef.orderByPriority(); + this.query = this.query.orderByPriority(); return this; } orderByValue(): Query { - if (this.query) { - throw new Error("You can't combine multiple orderBy calls!"); - } - this.query = this.dbRef.orderByValue(); + this.query = this.query.orderByValue(); return this; } @@ -1790,7 +1787,7 @@ class Query implements QueryBase { if (eventType === "value") { listener = new com.google.firebase.database.ValueEventListener({ onDataChange: (snapshot: com.google.firebase.database.DataSnapshot) => { - callback(firebase.getCallbackData('ValueChanged', snapshot)); + callback(nativeSnapshotToWebSnapshot(snapshot)); }, onCancelled: (databaseError: com.google.firebase.database.DatabaseError) => { callback({ @@ -1807,22 +1804,22 @@ class Query implements QueryBase { }, onChildAdded: (snapshot: com.google.firebase.database.DataSnapshot, previousChildKey: string) => { if (eventType === "child_added") { - callback(firebase.getCallbackData(eventType, snapshot)); + callback(nativeSnapshotToWebSnapshot(snapshot)); } }, onChildRemoved: (snapshot: com.google.firebase.database.DataSnapshot) => { if (eventType === "child_removed") { - callback(firebase.getCallbackData(eventType, snapshot)); + callback(nativeSnapshotToWebSnapshot(snapshot)); } }, onChildChanged: (snapshot: com.google.firebase.database.DataSnapshot, previousChildKey: string) => { if (eventType === "child_changed") { - callback(firebase.getCallbackData(eventType, snapshot)); + callback(nativeSnapshotToWebSnapshot(snapshot)); } }, onChildMoved: (snapshot: com.google.firebase.database.DataSnapshot, previousChildKey: string) => { if (eventType === "child_moved") { - callback(firebase.getCallbackData(eventType, snapshot)); + callback(nativeSnapshotToWebSnapshot(snapshot)); } } }); From 4d6a394212bf160ccc9a8847a8210d2a242f47e4 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Tue, 5 Mar 2019 13:33:32 -0500 Subject: [PATCH 10/18] [query][ios] - Make webapi queries return a datasnapshot that follows the webapi --- src/firebase.ios.ts | 89 +++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 47 deletions(-) diff --git a/src/firebase.ios.ts b/src/firebase.ios.ts index 283ec9b6..2dc4c37e 100755 --- a/src/firebase.ios.ts +++ b/src/firebase.ios.ts @@ -1395,55 +1395,60 @@ firebase.webQuery = (path: string): QueryBase => { }; class Query implements QueryBase { - private query: FIRDatabaseQuery; // Keep track of internal query state allowing us to chain filter/range/limit + private query: FIRDatabaseQuery | FIRDatabaseReference; // Keep track of internal query state allowing us to chain filter/range/limit private static eventListenerMap: Map> = new Map(); // A map to keep track all all the listeners attached for the specified eventType - constructor(private dbRef: FIRDatabaseReference, private path: string) { } + constructor(private dbRef: FIRDatabaseReference, private path: string) { + this.query = this.dbRef; + } on(eventType: string, callback: (a: any, b?: string) => any): Promise { const onValueEvent = result => { - if (result.error) { - callback(result); // CAREFUL before we were calling result.error! - } else { - callback({ - key: result.key, - val: () => result.value, - exists: () => !!result.value - }); - } + callback(result); }; return new Promise((resolve, reject) => { try { if (eventType === "value" || eventType === "child_added" || eventType === "child_changed" || eventType === "child_removed" || eventType === "child_moved") { - // This.query may not exist if we call on without any sorts - const firDatabaseHandle = this.query ? this.attachEventObserver(this.query, eventType, onValueEvent) : - this.attachEventObserver(this.dbRef, eventType, onValueEvent); + const firDataEventType = this.eventToFIRDataEventType(eventType); + const firDatabaseHandle = this.attachEventObserver(this.query, firDataEventType, onValueEvent); if (!Query.eventListenerMap.has(eventType)) { Query.eventListenerMap.set(eventType, []); } Query.eventListenerMap.get(eventType).push(firDatabaseHandle); // We need to keep track of the listeners to fully remove them when calling off } else { + callback({ + error: "Invalid eventType. Use one of the following: 'value', 'child_added', 'child_changed', 'child_removed', or 'child_moved'" + }); reject("Invalid eventType. Use one of the following: 'value', 'child_added', 'child_changed', 'child_removed', or 'child_moved'"); return; } resolve(); } catch (ex) { - console.log("Error in firebase.addValueEventListener: " + ex); + console.log("Error in firebase.on: " + ex); reject(ex); } }); } - once(eventType: string): Promise { + once(eventType: string): Promise { return new Promise((resolve, reject) => { - firebase.getValue(this.path).then(result => { - resolve({ - key: result.key, - val: () => result.value, - exists: () => !!result.value - }); - }); + try { + const firDataEventType = this.eventToFIRDataEventType(eventType); + this.query.observeEventTypeWithBlockWithCancelBlock( + firDataEventType, + snapshot => { + resolve(nativeSnapshotToWebSnapshot(snapshot)); + }, + firebaseError => { + reject({ + error: firebaseError.localizedDescription + }); + }); + } catch (ex) { + console.log("Error in firebase.once: " + ex); + reject(ex); + } }); } @@ -1461,34 +1466,22 @@ class Query implements QueryBase { } orderByChild(value: string): Query { - if (this.query) { - throw new Error("You can't combine multiple orderBy calls!"); - } - this.query = this.dbRef.queryOrderedByChild(value); + this.query = this.query.queryOrderedByChild(value); return this; } orderByKey(): Query { - if (this.query) { - throw new Error("You can't combine multiple orderBy calls!"); - } - this.query = this.dbRef.queryOrderedByKey(); + this.query = this.query.queryOrderedByKey(); return this; } orderByPriority(): Query { - if (this.query) { - throw new Error("You can't combine multiple orderBy calls!"); - } - this.query = this.dbRef.queryOrderedByPriority(); + this.query = this.query.queryOrderedByPriority(); return this; } orderByValue(): Query { - if (this.query) { - throw new Error("You can't combine multiple orderBy calls!"); - } - this.query = this.dbRef.queryOrderedByValue(); + this.query = this.query.queryOrderedByValue(); return this; } @@ -1532,12 +1525,7 @@ class Query implements QueryBase { return this; } - /** - * Depending on the eventType, attach listeners at the specified Database location. Follow the WebApi which listens - * to specific events (Android is more generic value / child - which includes all events add, change, remove etc). - * Similar to firebase._addObserver but I do not want to listen for every event - */ - private attachEventObserver(dbRef: FIRDatabaseQuery | FIRDatabaseReference, eventType: string, callback): number { + private eventToFIRDataEventType(eventType: string): FIRDataEventType { let firEventType: FIRDataEventType; switch (eventType) { case "value": @@ -1556,11 +1544,18 @@ class Query implements QueryBase { firEventType = FIRDataEventType.ChildMoved; break; } - + return firEventType; + } + /** + * Depending on the eventType, attach listeners at the specified Database location. Follow the WebApi which listens + * to specific events (Android is more generic value / child - which includes all events add, change, remove etc). + * Similar to firebase._addObserver but I do not want to listen for every event + */ + private attachEventObserver(dbRef: FIRDatabaseQuery | FIRDatabaseReference, firEventType: FIRDataEventType, callback): number { const listener = dbRef.observeEventTypeWithBlockWithCancelBlock( firEventType, snapshot => { - callback(firebase.getCallbackData(eventType, snapshot)); + callback(nativeSnapshotToWebSnapshot(snapshot)); }, firebaseError => { callback({ From 9d08715d65e8efebb40c4a3d6fb96ddc7cda2e8f Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Tue, 5 Mar 2019 13:53:04 -0500 Subject: [PATCH 11/18] [docs][database] - Update webapi query documentation --- docs/DATABASE.md | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/docs/DATABASE.md b/docs/DATABASE.md index 7572f054..d2966c39 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -227,13 +227,24 @@ Alternatively you can use the web api to query data. See [docs](https://firebase Some key notes: +The DataSnapshot returned is vastly different from the native api's snapshot! Please follow the web api docs to see what +you can do with the datasnapshot returned. Note that Datasnapshot.ref() is yet implemented. + +`Query.on()` does not accept a cancelCallbackOrContext. Similar to the native api, check if result.error is true before continuing. + +`once("eventType")` behaves differently on Android and iOS. On Android once only works with an eventType of `value` whereas +iOS will work with all the eventTypes like `child_added, child_removed` etc. + `off("eventType")` will remove all listeners for "eventType" at the given path. So you do not need to call `off()` the same number of times you call `on()`. Listeners for all eventTypes will be removed if no eventType is provided. -Filters (equalTo, starAt, endAt, LimitBy, etc) are only usable after you chain it with a sort. (While Firebase exposes these without doing -a sort, your callback is never called). Think about it, if you apply equalTo without an orderBy what are you checking key, value, priority ??? +Filters (`equalTo, startAt, endAt, LimitBy`, etc) should be used with a sort. If not, you may not get the result expected. +If you apply equalTo without an orderBy what are you checking for (key, value, priority)? -DO NOT try to apply more than one orderBy to the same query as this will throw (follows the api) +When using `equalTo, startAt or endAt` chained with `orderByKey()`, you MUST make sure they are all strings. Otherwise expect +an exception to be thrown. + +DO NOT try to apply more than one orderBy to the same query as this will crash the application (follows the api) ```typescript const bad = firebaseWebApi.database().ref(path).orderByKey(); bad.orderByValue(); // <------ will throw here! @@ -251,7 +262,23 @@ DO NOT try to apply more than one orderBy to the same query as this will throw ( // You can also do the following firebase.webQuery("/companies").orderByKey().on("value", onQueryEvent); + + const onQueryEvent = (result: any) { + if (!result.error) { + console.log("Exists: " + result.exists()); + console.log("Key: " + result.key); + console.log("Value: " + JSON.stringify(result.val())); + result.forEach( + snapshot => { + // Do something forEach children. Note that this goes one level deep + console.log(snapshot.toJSON()); + } + ); + } + }; + ``` +Since the webapi queries follow the Google Documentation you can look at their examples for more reference. ### update From 1409b250ef66a17d7997b0271f23b7cd58b8c70a Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Tue, 5 Mar 2019 13:58:30 -0500 Subject: [PATCH 12/18] [chore] - Remove short references as they are deprecated in later versions of Nativescript --- demo/app/vendor-platform.android.ts | 6 +++--- demo/app/vendor.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/demo/app/vendor-platform.android.ts b/demo/app/vendor-platform.android.ts index 16352227..0b99ee56 100644 --- a/demo/app/vendor-platform.android.ts +++ b/demo/app/vendor-platform.android.ts @@ -1,9 +1,9 @@ -require("application"); +require("tns-core-modules/application"); if (!global["__snapshot"]) { // In case snapshot generation is enabled these modules will get into the bundle // but will not be required/evaluated. // The snapshot webpack plugin will add them to the tns-java-classes.js bundle file. // This way, they will be evaluated on app start as early as possible. - require("ui/frame"); - require("ui/frame/activity"); + require("tns-core-modules/ui/frame"); + require("tns-core-modules/ui/frame/activity"); } diff --git a/demo/app/vendor.ts b/demo/app/vendor.ts index 8a381374..146389a1 100644 --- a/demo/app/vendor.ts +++ b/demo/app/vendor.ts @@ -1,6 +1,6 @@ // Snapshot the ~/app.css and the theme -const application = require("application"); -require("ui/styling/style-scope"); +const application = require("tns-core-modules/application"); +require("tns-core-modules/ui/styling/style-scope"); const appCssContext = require.context("~/", false, /^\.\/app\.(css|scss|less|sass)$/); global.registerWebpackModules(appCssContext); application.loadAppCss(); From a5fcb637e566322fb3eb1a9f593808cd73ab010c Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Tue, 5 Mar 2019 14:55:25 -0500 Subject: [PATCH 13/18] [chore] - White space --- src/firebase.d.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/firebase.d.ts b/src/firebase.d.ts index d2e672b8..93e5bbcd 100644 --- a/src/firebase.d.ts +++ b/src/firebase.d.ts @@ -546,24 +546,34 @@ export interface OnDisconnect { update(values: Object): Promise; } + // WebAPI Query export interface Query { on(eventType: string, callback: (a: any, b?: string) => any): Promise; + once(eventType: string): Promise; + off(eventType?: string, callback?: (a: DataSnapshot, b?: string | null) => any, context?: Object | null): void; + orderByChild(value: string): Query; + orderByKey(): Query; + orderByPriority(): Query; + orderByValue(): Query; + equalTo(value: string | number | boolean, key?: string): Query; + startAt(value: string | number | boolean, key?: string): Query; + endAt(value: string | number | boolean, key?: string): Query; + limitToFirst(value: number): Query; + limitToLast(value: number): Query; } -export function webQuery(path: string): Query; - export interface DataSnapshot { key: string; ref: any; // TODO: Type it so that it returns a databaseReference. @@ -615,6 +625,8 @@ export function removeEventListeners(listeners: Array, path: string): Promi export function onDisconnect(path: string): OnDisconnect; +export function webQuery(path: string): Query; + export function enableLogging(logger?: boolean | ((a: string) => any), persistent?: boolean); /** From c02d7ae42f680e3318d8a897b29b8c8b745d3913 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Tue, 5 Mar 2019 15:17:12 -0500 Subject: [PATCH 14/18] [chore] - Fix typing errors --- src/app/auth/index.ts | 2 +- src/app/database/index.ts | 3 +-- src/app/storage/index.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/auth/index.ts b/src/app/auth/index.ts index 19fae462..67976ca3 100644 --- a/src/app/auth/index.ts +++ b/src/app/auth/index.ts @@ -4,7 +4,7 @@ import { FirebaseEmailLinkActionCodeSettings, LoginType, User } from "../../fire export module auth { export class Auth { private authStateChangedHandler; - public currentUser: User; + public currentUser: User | undefined; public languageCode: string | null; public onAuthStateChanged(handler: (user: User) => void): void { diff --git a/src/app/database/index.ts b/src/app/database/index.ts index 527b2c4f..1da28f42 100644 --- a/src/app/database/index.ts +++ b/src/app/database/index.ts @@ -1,5 +1,4 @@ import * as firebase from "../../firebase"; -import { AddEventListenerResult, FBData } from "../../firebase"; import { nextPushId } from "./util/NextPushId"; export namespace database { @@ -94,7 +93,7 @@ export namespace database { * Creates a Query with the specified starting point. The value to start at should match the type * passed to orderBy(). If using orderByKey(), the value must be a string */ - public startAt(value: number | string | boolean | null): firebase.Query { + public startAt(value: number | string | boolean): firebase.Query { return this.queryObject.startAt(value); } diff --git a/src/app/storage/index.ts b/src/app/storage/index.ts index a648cc22..691a7d55 100644 --- a/src/app/storage/index.ts +++ b/src/app/storage/index.ts @@ -11,7 +11,7 @@ export module storage { export class Reference { - private path: string; + private path: string | undefined; parent: Reference | null; // TODO set this every time we navigate.. root: Reference; From 6244c7aaf752b61f3716b23385fd74aace5a1491 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Thu, 7 Mar 2019 11:34:53 -0500 Subject: [PATCH 15/18] [query] - Update query.on() to follow web api and returns a Function instead of a promise - Also remove ref from datasnapshot until we translate it into a web ref instead of native ref. --- src/app/database/index.ts | 7 ++--- src/firebase.android.ts | 60 +++++++++++++++++++-------------------- src/firebase.d.ts | 4 +-- src/firebase.ios.ts | 13 ++++----- 4 files changed, 39 insertions(+), 45 deletions(-) diff --git a/src/app/database/index.ts b/src/app/database/index.ts index 1da28f42..be202aa9 100644 --- a/src/app/database/index.ts +++ b/src/app/database/index.ts @@ -17,7 +17,7 @@ export namespace database { val(): any; } - export class Query { + export class Query implements firebase.Query { protected path: string; private queryObject: firebase.Query; constructor(path: string) { @@ -34,9 +34,8 @@ export namespace database { public on(eventType: string, callback: (a: DataSnapshot | null, b?: string) => any, cancelCallbackOrContext?: Object | null, context?: Object | null): (a: DataSnapshot | null, b?: string) => Function { - this.queryObject.on(eventType, callback).catch(error => { - console.log("firebase.database().on error: " + error); - }); + this.queryObject.on(eventType, callback); + return callback; // According to firebase doc we just return the callback given } diff --git a/src/firebase.android.ts b/src/firebase.android.ts index b135b227..125ff022 100755 --- a/src/firebase.android.ts +++ b/src/firebase.android.ts @@ -1640,41 +1640,36 @@ class Query implements QueryBase { this.query = this.dbRef; } - on(eventType: string, callback: (a: any, b?: string) => any): Promise { + on(eventType: string, callback: (a: any, b?: string) => any): Function { const onValueEvent = result => { - callback(result); + callback(result); }; - return new Promise((resolve, reject) => { - try { - if (firebase.instance === null) { - reject("Run init() first!"); - return; - } - const listener = this.createEventListener(eventType, onValueEvent); + try { + if (firebase.instance === null) { + throw new Error("Run init() first!"); + } + const listener = this.createEventListener(eventType, onValueEvent); - if (eventType === "value") { - this.query.addValueEventListener(listener as com.google.firebase.database.ValueEventListener); - } else if (eventType === "child_added" || eventType === "child_changed" || eventType === "child_removed" || eventType === "child_moved") { - this.query.addChildEventListener(listener as com.google.firebase.database.ChildEventListener); - } else { - callback({ - error: "Invalid eventType. Use one of the following: 'value', 'child_added', 'child_changed', 'child_removed', or 'child_moved'" - }); - reject("Invalid eventType. Use one of the following: 'value', 'child_added', 'child_changed', 'child_removed', or 'child_moved'"); - return; - } - // Add listener to our map which keeps track of eventType: child/value events - if (!Query.eventListenerMap.has(eventType)) { - Query.eventListenerMap.set(eventType, []); - } - Query.eventListenerMap.get(eventType).push(listener); // We need to keep track of the listeners to fully remove them when calling off - resolve(); - } catch (ex) { - console.log("Error in firebase.on: " + ex); - reject(ex); + if (eventType === "value") { + this.query.addValueEventListener(listener as com.google.firebase.database.ValueEventListener); + } else if (eventType === "child_added" || eventType === "child_changed" || eventType === "child_removed" || eventType === "child_moved") { + this.query.addChildEventListener(listener as com.google.firebase.database.ChildEventListener); + } else { + callback({ + error: "Invalid eventType. Use one of the following: 'value', 'child_added', 'child_changed', 'child_removed', or 'child_moved'" + }); } - }); + // Add listener to our map which keeps track of eventType: child/value events + if (!Query.eventListenerMap.has(eventType)) { + Query.eventListenerMap.set(eventType, []); + } + Query.eventListenerMap.get(eventType).push(listener); // We need to keep track of the listeners to fully remove them when calling off + } catch (ex) { + console.error("Error in firebase.on: " + ex); + } finally { + return callback; + } } once(eventType: string): Promise { @@ -2116,6 +2111,9 @@ firebase.transaction = (path: string, transactionUpdate: (currentState) => any, }); }; +function nativeRefToWebRef(ref: com.google.firebase.database.DatabaseReference) { + +} // Converts Android DataSnapshot into Web Datasnapshot function nativeSnapshotToWebSnapshot(snapshot: com.google.firebase.database.DataSnapshot): DataSnapshot { function forEach(action: (datasnapshot: DataSnapshot) => any): boolean { @@ -2131,7 +2129,7 @@ function nativeSnapshotToWebSnapshot(snapshot: com.google.firebase.database.Data return { key: snapshot.getKey(), - ref: snapshot.getRef(), + // ref: snapshot.getRef(), TODO: Convert native ref to webRef child: (path: string) => nativeSnapshotToWebSnapshot(snapshot.child(path)), exists: () => snapshot.exists(), forEach: (func: (datasnapshot) => any) => forEach(func), diff --git a/src/firebase.d.ts b/src/firebase.d.ts index 93e5bbcd..3b7e45b0 100644 --- a/src/firebase.d.ts +++ b/src/firebase.d.ts @@ -549,7 +549,7 @@ export interface OnDisconnect { // WebAPI Query export interface Query { - on(eventType: string, callback: (a: any, b?: string) => any): Promise; + on(eventType: string, callback: (a: any, b?: string) => any): Function; once(eventType: string): Promise; @@ -576,7 +576,7 @@ export interface Query { export interface DataSnapshot { key: string; - ref: any; // TODO: Type it so that it returns a databaseReference. + // ref: any; // TODO: It's not properly typed and returns a native Ref which will lead to errors child(path: string): DataSnapshot; exists(): boolean; diff --git a/src/firebase.ios.ts b/src/firebase.ios.ts index 2dc4c37e..702a012f 100755 --- a/src/firebase.ios.ts +++ b/src/firebase.ios.ts @@ -1402,11 +1402,10 @@ class Query implements QueryBase { this.query = this.dbRef; } - on(eventType: string, callback: (a: any, b?: string) => any): Promise { + on(eventType: string, callback: (a: any, b?: string) => any): Function { const onValueEvent = result => { callback(result); }; - return new Promise((resolve, reject) => { try { if (eventType === "value" || eventType === "child_added" || eventType === "child_changed" || eventType === "child_removed" || eventType === "child_moved") { @@ -1420,15 +1419,13 @@ class Query implements QueryBase { callback({ error: "Invalid eventType. Use one of the following: 'value', 'child_added', 'child_changed', 'child_removed', or 'child_moved'" }); - reject("Invalid eventType. Use one of the following: 'value', 'child_added', 'child_changed', 'child_removed', or 'child_moved'"); - return; } - resolve(); } catch (ex) { console.log("Error in firebase.on: " + ex); - reject(ex); } - }); + finally { + return callback; + } } once(eventType: string): Promise { @@ -1838,7 +1835,7 @@ function nativeSnapshotToWebSnapshot(snapshot: FIRDataSnapshot): DataSnapshot { return { key: snapshot.key, - ref: snapshot.ref, + // ref: snapshot.ref, child: (path: string) => nativeSnapshotToWebSnapshot(snapshot.childSnapshotForPath(path)), exists: () => snapshot.exists(), forEach: (func: (datasnapshot) => any) => forEach(func), From 7ffc3026e08d3e437b46d1f6072e072f0822e3b1 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Thu, 7 Mar 2019 14:25:47 -0500 Subject: [PATCH 16/18] [query] - Accept errorCallback for query.on() --- src/app/database/index.ts | 20 +++------------ src/firebase.android.ts | 31 +++++++++++------------- src/firebase.d.ts | 2 +- src/firebase.ios.ts | 51 +++++++++++++++++++-------------------- 4 files changed, 44 insertions(+), 60 deletions(-) diff --git a/src/app/database/index.ts b/src/app/database/index.ts index be202aa9..fee5c05b 100644 --- a/src/app/database/index.ts +++ b/src/app/database/index.ts @@ -2,20 +2,7 @@ import * as firebase from "../../firebase"; import { nextPushId } from "./util/NextPushId"; export namespace database { - export interface DataSnapshot { - // child(path: string): DataSnapshot; - exists(): boolean; - // exportVal(): any; - // forEach(action: (a: DataSnapshot) => boolean): boolean; - // getPriority(): string | number | null; - // hasChild(path: string): boolean; - // hasChildren(): boolean; - key: string | null; - // numChildren(): number; - // ref: Reference; - // toJSON(): Object | null; - val(): any; - } + export type DataSnapshot = firebase.DataSnapshot; export class Query implements firebase.Query { protected path: string; @@ -29,12 +16,13 @@ export namespace database { * Listens for data changes at a particular location * @param eventType One of the following strings: "value", "child_added", "child_changed", "child_removed", or "child_moved." * @param callback A callback that fires when the specified event occurs. The callback will be passed a DataSnapshot. + * @param cancelCallbackOrContext A callback that fires when an error occurs. The callback will be passed an error object. * @returns The provided callback function is returned unmodified. */ public on(eventType: string, callback: (a: DataSnapshot | null, b?: string) => any, - cancelCallbackOrContext?: Object | null, context?: Object | null): (a: DataSnapshot | null, b?: string) => Function { + cancelCallbackOrContext?: (a: Error | null) => any, context?: Object | null): (a: DataSnapshot | null, b?: string) => Function { - this.queryObject.on(eventType, callback); + this.queryObject.on(eventType, callback, cancelCallbackOrContext); return callback; // According to firebase doc we just return the callback given } diff --git a/src/firebase.android.ts b/src/firebase.android.ts index 125ff022..43bca008 100755 --- a/src/firebase.android.ts +++ b/src/firebase.android.ts @@ -1640,25 +1640,19 @@ class Query implements QueryBase { this.query = this.dbRef; } - on(eventType: string, callback: (a: any, b?: string) => any): Function { - const onValueEvent = result => { - callback(result); - }; - + on(eventType: string, callback: (a: DataSnapshot, b?: string) => any, cancelCallbackOrContext?: (a: Error | null) => any): Function { try { if (firebase.instance === null) { throw new Error("Run init() first!"); } - const listener = this.createEventListener(eventType, onValueEvent); + const listener = this.createEventListener(eventType, callback, cancelCallbackOrContext); if (eventType === "value") { this.query.addValueEventListener(listener as com.google.firebase.database.ValueEventListener); } else if (eventType === "child_added" || eventType === "child_changed" || eventType === "child_removed" || eventType === "child_moved") { this.query.addChildEventListener(listener as com.google.firebase.database.ChildEventListener); } else { - callback({ - error: "Invalid eventType. Use one of the following: 'value', 'child_added', 'child_changed', 'child_removed', or 'child_moved'" - }); + throw new Error(`${eventType} is not a valid eventType. Use one of the following: 'value', 'child_added', 'child_changed', 'child_removed', or 'child_moved'`); } // Add listener to our map which keeps track of eventType: child/value events if (!Query.eventListenerMap.has(eventType)) { @@ -1667,6 +1661,9 @@ class Query implements QueryBase { Query.eventListenerMap.get(eventType).push(listener); // We need to keep track of the listeners to fully remove them when calling off } catch (ex) { console.error("Error in firebase.on: " + ex); + if (cancelCallbackOrContext !== undefined) { + cancelCallbackOrContext(ex); + } } finally { return callback; } @@ -1693,7 +1690,7 @@ class Query implements QueryBase { firebase.instance.child(this.path).addListenerForSingleValueEvent(listener); } catch (ex) { - console.log("Error in firebase.once: " + ex); + console.error("Error in firebase.once: " + ex); reject(ex); } }); @@ -1776,7 +1773,7 @@ class Query implements QueryBase { * to specific events (Android is more generic value / child - which includes all events add, change, remove etc). * Similar to firebase._addObserver but I do not want to listen for every event */ - private createEventListener(eventType: string, callback): com.google.firebase.database.ValueEventListener | com.google.firebase.database.ChildEventListener { + private createEventListener(eventType: string, callback, cancelCallback?): com.google.firebase.database.ValueEventListener | com.google.firebase.database.ChildEventListener { let listener; if (eventType === "value") { @@ -1785,17 +1782,17 @@ class Query implements QueryBase { callback(nativeSnapshotToWebSnapshot(snapshot)); }, onCancelled: (databaseError: com.google.firebase.database.DatabaseError) => { - callback({ - error: databaseError.getMessage() - }); + if (cancelCallback !== undefined) { + cancelCallback(new Error(databaseError.getMessage())); + } } }); } else if (eventType === "child_added" || eventType === "child_changed" || eventType === "child_removed" || eventType === "child_moved") { listener = new com.google.firebase.database.ChildEventListener({ onCancelled: (databaseError: com.google.firebase.database.DatabaseError) => { - callback({ - error: databaseError.getMessage() - }); + if (cancelCallback !== undefined) { + cancelCallback(new Error(databaseError.getMessage())); + } }, onChildAdded: (snapshot: com.google.firebase.database.DataSnapshot, previousChildKey: string) => { if (eventType === "child_added") { diff --git a/src/firebase.d.ts b/src/firebase.d.ts index 3b7e45b0..6680dd2d 100644 --- a/src/firebase.d.ts +++ b/src/firebase.d.ts @@ -549,7 +549,7 @@ export interface OnDisconnect { // WebAPI Query export interface Query { - on(eventType: string, callback: (a: any, b?: string) => any): Function; + on(eventType: string, callback: (a: any, b?: string) => any, cancelCallbackOrContext?: (a: Error | null) => any): Function; once(eventType: string): Promise; diff --git a/src/firebase.ios.ts b/src/firebase.ios.ts index 702a012f..b742c9fc 100755 --- a/src/firebase.ios.ts +++ b/src/firebase.ios.ts @@ -1402,30 +1402,29 @@ class Query implements QueryBase { this.query = this.dbRef; } - on(eventType: string, callback: (a: any, b?: string) => any): Function { - const onValueEvent = result => { - callback(result); - }; - try { - if (eventType === "value" || eventType === "child_added" || eventType === "child_changed" - || eventType === "child_removed" || eventType === "child_moved") { - const firDataEventType = this.eventToFIRDataEventType(eventType); - const firDatabaseHandle = this.attachEventObserver(this.query, firDataEventType, onValueEvent); - if (!Query.eventListenerMap.has(eventType)) { - Query.eventListenerMap.set(eventType, []); - } - Query.eventListenerMap.get(eventType).push(firDatabaseHandle); // We need to keep track of the listeners to fully remove them when calling off - } else { - callback({ - error: "Invalid eventType. Use one of the following: 'value', 'child_added', 'child_changed', 'child_removed', or 'child_moved'" - }); + on(eventType: string, callback: (a: any, b?: string) => any, cancelCallbackOrContext?: (a: Error | null) => any): Function { + try { + if (eventType === "value" || eventType === "child_added" || eventType === "child_changed" + || eventType === "child_removed" || eventType === "child_moved") { + const firDataEventType = this.eventToFIRDataEventType(eventType); + const firDatabaseHandle = this.attachEventObserver(this.query, firDataEventType, callback, cancelCallbackOrContext); + if (!Query.eventListenerMap.has(eventType)) { + Query.eventListenerMap.set(eventType, []); } - } catch (ex) { - console.log("Error in firebase.on: " + ex); + Query.eventListenerMap.get(eventType).push(firDatabaseHandle); // We need to keep track of the listeners to fully remove them when calling off + } else { + throw new Error(`${eventType} is not a valid eventType. Use one of the following: 'value', 'child_added', 'child_changed', 'child_removed', or 'child_moved'`); } - finally { - return callback; + } catch (ex) { + // TODO: Make custom errors + console.error("Error in firebase.on: " + ex); + if (cancelCallbackOrContext !== undefined) { + cancelCallbackOrContext(ex); } + } + finally { + return callback; + } } once(eventType: string): Promise { @@ -1443,7 +1442,7 @@ class Query implements QueryBase { }); }); } catch (ex) { - console.log("Error in firebase.once: " + ex); + console.error("Error in firebase.once: " + ex); reject(ex); } }); @@ -1548,16 +1547,16 @@ class Query implements QueryBase { * to specific events (Android is more generic value / child - which includes all events add, change, remove etc). * Similar to firebase._addObserver but I do not want to listen for every event */ - private attachEventObserver(dbRef: FIRDatabaseQuery | FIRDatabaseReference, firEventType: FIRDataEventType, callback): number { + private attachEventObserver(dbRef: FIRDatabaseQuery | FIRDatabaseReference, firEventType: FIRDataEventType, callback, cancelCallback): number { const listener = dbRef.observeEventTypeWithBlockWithCancelBlock( firEventType, snapshot => { callback(nativeSnapshotToWebSnapshot(snapshot)); }, firebaseError => { - callback({ - error: firebaseError.localizedDescription - }); + if (cancelCallback !== undefined) { + cancelCallback(new Error(firebaseError.localizedDescription)); + } }); return listener; } From 414197ffba56002f9458a3c918d1b1a520beeb35 Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Thu, 7 Mar 2019 14:26:25 -0500 Subject: [PATCH 17/18] [docs][database] - Update webapi queries to show error callbacks --- docs/DATABASE.md | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/docs/DATABASE.md b/docs/DATABASE.md index d2966c39..974b68d3 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -371,17 +371,25 @@ The link is for the iOS SDK, but it's the same for Android. Web API ```js - const onValueEvent = result => { - if (result.error) { - console.log("Listener error: " + result.error); - } else { - console.log("Key: " + result.key); - console.log("key exists? " + result.exists()); - console.log("Value: " + JSON.stringify(result.val())); - } - }; + public doWebAddValueEventListenerForCompanies(): void { + const path = "/companies"; + const onValueEvent = (result: firebase.DataSnapshot ) => { + // NOTE: we no longer check for result.error as it doesn't exist. Pass in an onError callback to handle errors! + console.log("value : " + result.forEach(datasnapshot => { + console.log(datasnapshot.key + " " + JSON.stringify(datasnapshot.val())); + })); + console.log("key exists? " + result.exists()); + this.set("path", path); + this.set("key", result.key); + this.set("value", JSON.stringify(result.val())); + }; + + const onErrorEvent = (err: Error ) => { + console.log("Encountered an error: " + err); + }; + firebaseWebApi.database().ref("/companies").on("value", onValueEvent, onErrorEvent /* Totally Optional */); + } - firebaseWebApi.database().ref("/companies").on("value", onValueEvent); ``` From 1e7f884a77172144a68203b7e6510594fe00b7fe Mon Sep 17 00:00:00 2001 From: Kevin Li Date: Thu, 3 Oct 2019 13:29:45 -0400 Subject: [PATCH 18/18] [query] - Implement query.off correctly to allow removal of callbacks --- src/app/database/index.ts | 2 +- src/firebase.android.ts | 17 ++++++++++++++--- src/firebase.ios.ts | 15 +++++++++++++-- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/app/database/index.ts b/src/app/database/index.ts index fee5c05b..efb60d49 100644 --- a/src/app/database/index.ts +++ b/src/app/database/index.ts @@ -33,7 +33,7 @@ export namespace database { */ public off(eventType?: string, callback?: (a: DataSnapshot, b?: string | null) => any, context?: Object | null): void { // TODO: use callback rather than remove ALL listeners for a given eventType - this.queryObject.off(eventType); + this.queryObject.off(eventType, callback); } /** diff --git a/src/firebase.android.ts b/src/firebase.android.ts index 43bca008..0e7af109 100755 --- a/src/firebase.android.ts +++ b/src/firebase.android.ts @@ -1634,6 +1634,7 @@ firebase.webQuery = (path: string): QueryBase => { class Query implements QueryBase { private query: com.google.firebase.database.Query; // Keep track of internal query state allowing us to chain filter/range/limit + private internalListenerMap: Map> = new Map(); // A map to keep track of callbacks to this specific Query Object private static eventListenerMap: Map> = new Map(); // A map to keep track all all the listeners attached for the specified eventType constructor(private dbRef: com.google.firebase.database.DatabaseReference, private path: string) { @@ -1654,6 +1655,12 @@ class Query implements QueryBase { } else { throw new Error(`${eventType} is not a valid eventType. Use one of the following: 'value', 'child_added', 'child_changed', 'child_removed', or 'child_moved'`); } + + if (!this.internalListenerMap.has(callback)) { + this.internalListenerMap.set(callback, []); + } + this.internalListenerMap.get(callback).push(listener); // Incase someone uses the same callback multiple times on same query + // Add listener to our map which keeps track of eventType: child/value events if (!Query.eventListenerMap.has(eventType)) { Query.eventListenerMap.set(eventType, []); @@ -1696,14 +1703,18 @@ class Query implements QueryBase { }); } - off(eventType?: string): void { + off(eventType?: string, callback?: (a: DataSnapshot, b?: string | null) => any): void { // Remove all events if none specified if (!eventType) { Query.eventListenerMap.forEach((value: any[], key: string) => { firebase.removeEventListeners(value, this.path); }); - } else { // Remove only the event specified by the user - if (Query.eventListenerMap.get(eventType)) { + } else { + if (callback) { + if (this.internalListenerMap.has(callback)) { + firebase.removeEventListeners(this.internalListenerMap.get(callback), this.path); + } + } else if (Query.eventListenerMap.get(eventType)) { // Remove only the event specified by the user firebase.removeEventListeners(Query.eventListenerMap.get(eventType), this.path); } } diff --git a/src/firebase.ios.ts b/src/firebase.ios.ts index b742c9fc..db50cf90 100755 --- a/src/firebase.ios.ts +++ b/src/firebase.ios.ts @@ -1396,6 +1396,7 @@ firebase.webQuery = (path: string): QueryBase => { class Query implements QueryBase { private query: FIRDatabaseQuery | FIRDatabaseReference; // Keep track of internal query state allowing us to chain filter/range/limit + private internalListenerMap: Map> = new Map(); // A map to keep track of callbacks to this specific Query Object private static eventListenerMap: Map> = new Map(); // A map to keep track all all the listeners attached for the specified eventType constructor(private dbRef: FIRDatabaseReference, private path: string) { @@ -1408,6 +1409,12 @@ class Query implements QueryBase { || eventType === "child_removed" || eventType === "child_moved") { const firDataEventType = this.eventToFIRDataEventType(eventType); const firDatabaseHandle = this.attachEventObserver(this.query, firDataEventType, callback, cancelCallbackOrContext); + + if (!this.internalListenerMap.has(callback)) { + this.internalListenerMap.set(callback, []); + } + this.internalListenerMap.get(callback).push(firDatabaseHandle); // Incase someone uses the same callback multiple times on same query + if (!Query.eventListenerMap.has(eventType)) { Query.eventListenerMap.set(eventType, []); } @@ -1448,14 +1455,18 @@ class Query implements QueryBase { }); } - off(eventType?: string): void { + off(eventType?: string, callback?: (a: DataSnapshot, b?: string | null) => any): void { // Remove all events if none specified if (!eventType) { Query.eventListenerMap.forEach((value: any[], key: string) => { firebase.removeEventListeners(value, this.path); }); } else { // Remove only the event specified by the user - if (Query.eventListenerMap.get(eventType)) { + if (callback) { + if (this.internalListenerMap.has(callback)) { + firebase.removeEventListeners(this.internalListenerMap.get(callback), this.path); + } + } else if (Query.eventListenerMap.get(eventType)) { firebase.removeEventListeners(Query.eventListenerMap.get(eventType), this.path); } }