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),