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(); diff --git a/docs/DATABASE.md b/docs/DATABASE.md index 288c451a..974b68d3 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,67 @@ 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: + +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, 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)? + +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! + + // 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); + + 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 Changes the values of the keys specified in the dictionary without overwriting other keys at this location. @@ -307,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); ``` 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 892e431c..efb60d49 100644 --- a/src/app/database/index.ts +++ b/src/app/database/index.ts @@ -1,154 +1,116 @@ import * as firebase from "../../firebase"; -import { AddEventListenerResult, FBData } from "../../firebase"; import { nextPushId } from "./util/NextPushId"; -export module 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 class Query { - private static registeredListeners: Map> = new Map(); - private static registeredCallbacks: Map any>> = new Map(); +export namespace database { + export type DataSnapshot = firebase.DataSnapshot; + export class Query implements firebase.Query { 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); - } - ); + /** + * 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?: (a: Error | null) => any, context?: Object | null): (a: DataSnapshot | null, b?: string) => Function { - // 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); + this.queryObject.on(eventType, callback, cancelCallbackOrContext); - return null; + 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, callback); } - 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); + } + /** + * 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 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 priority + */ + public orderByPriority(): firebase.Query { + return this.queryObject.orderByPriority(); } - public orderByKey(): Query { - firebase.query( - this.getOnValueEventHandler(), - this.path, - { - orderBy: { - type: firebase.QueryOrderByType.KEY - } - } - ); - 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 orderByPriority(): Query { - firebase.query( - this.getOnValueEventHandler(), - this.path, - { - orderBy: { - type: firebase.QueryOrderByType.PRIORITY - } - } - ); - return this; + /** + * 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): firebase.Query { + return this.queryObject.startAt(value); } - public orderByValue(): Query { - firebase.query( - this.getOnValueEventHandler(), - this.path, - { - orderBy: { - type: firebase.QueryOrderByType.VALUE - } - } - ); - return this; + /** + * 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); } } @@ -258,8 +220,7 @@ export module database { } } - export interface ThenableReference extends Reference /*, PromiseLike */ - { + export interface ThenableReference extends Reference /*, PromiseLike */ { } export class Database { 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" }; +} 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; diff --git a/src/firebase.android.ts b/src/firebase.android.ts index 4515c401..0e7af109 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 { @@ -1421,22 +1422,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)); } }); @@ -1502,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,8 +1623,216 @@ firebase.update = (path, val) => { }); }; -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 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) { + this.query = this.dbRef; + } + + 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, 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 { + 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, []); + } + 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; + } + } + + once(eventType: string): Promise { + return new Promise((resolve, reject) => { + 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.error("Error in firebase.once: " + ex); + reject(ex); + } + }); + } + + 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 { + 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); + } + } + } + + orderByChild(value: string): Query { + this.query = this.query.orderByChild(value); + return this; + } + + orderByKey(): Query { + this.query = this.query.orderByKey(); + return this; + } + + orderByPriority(): Query { + this.query = this.query.orderByPriority(); + return this; + } + + orderByValue(): Query { + this.query = this.query.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, cancelCallback?): 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(nativeSnapshotToWebSnapshot(snapshot)); + }, + onCancelled: (databaseError: com.google.firebase.database.DatabaseError) => { + 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) => { + if (cancelCallback !== undefined) { + cancelCallback(new Error(databaseError.getMessage())); + } + }, + onChildAdded: (snapshot: com.google.firebase.database.DataSnapshot, previousChildKey: string) => { + if (eventType === "child_added") { + callback(nativeSnapshotToWebSnapshot(snapshot)); + } + }, + onChildRemoved: (snapshot: com.google.firebase.database.DataSnapshot) => { + if (eventType === "child_removed") { + callback(nativeSnapshotToWebSnapshot(snapshot)); + } + }, + onChildChanged: (snapshot: com.google.firebase.database.DataSnapshot, previousChildKey: string) => { + if (eventType === "child_changed") { + callback(nativeSnapshotToWebSnapshot(snapshot)); + } + }, + onChildMoved: (snapshot: com.google.firebase.database.DataSnapshot, previousChildKey: string) => { + if (eventType === "child_moved") { + callback(nativeSnapshotToWebSnapshot(snapshot)); + } + } + }); + } + return listener; + } +} + +firebase.query = (updateCallback, path, options) => { + return new Promise((resolve, reject) => { try { if (firebase.instance === null) { reject("Run init() first!"); @@ -1910,6 +2119,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 { @@ -1925,7 +2137,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 28bda5e2..6680dd2d 100644 --- a/src/firebase.d.ts +++ b/src/firebase.d.ts @@ -547,9 +547,36 @@ export interface OnDisconnect { update(values: Object): Promise; } +// WebAPI Query +export interface Query { + on(eventType: string, callback: (a: any, b?: string) => any, cancelCallbackOrContext?: (a: Error | null) => any): Function; + + 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 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; @@ -598,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); /** diff --git a/src/firebase.ios.ts b/src/firebase.ios.ts index 8efb1495..db50cf90 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,193 @@ 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 | 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) { + this.query = this.dbRef; + } + + 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 (!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, []); + } + 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'`); + } + } 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 { + return new Promise((resolve, reject) => { + try { + const firDataEventType = this.eventToFIRDataEventType(eventType); + this.query.observeEventTypeWithBlockWithCancelBlock( + firDataEventType, + snapshot => { + resolve(nativeSnapshotToWebSnapshot(snapshot)); + }, + firebaseError => { + reject({ + error: firebaseError.localizedDescription + }); + }); + } catch (ex) { + console.error("Error in firebase.once: " + ex); + reject(ex); + } + }); + } + + 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 (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); + } + } + } + + orderByChild(value: string): Query { + this.query = this.query.queryOrderedByChild(value); + return this; + } + + orderByKey(): Query { + this.query = this.query.queryOrderedByKey(); + return this; + } + + orderByPriority(): Query { + this.query = this.query.queryOrderedByPriority(); + return this; + } + + orderByValue(): Query { + this.query = this.query.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; + } + + private eventToFIRDataEventType(eventType: string): FIRDataEventType { + 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; + } + 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, cancelCallback): number { + const listener = dbRef.observeEventTypeWithBlockWithCancelBlock( + firEventType, + snapshot => { + callback(nativeSnapshotToWebSnapshot(snapshot)); + }, + firebaseError => { + if (cancelCallback !== undefined) { + cancelCallback(new Error(firebaseError.localizedDescription)); + } + }); + return listener; + } +} firebase.query = (updateCallback: (data: FBDataSingleEvent) => void, path: string, options: QueryOptions): Promise => { return new Promise((resolve, reject) => { @@ -1657,7 +1845,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),