Skip to content
This repository was archived by the owner on Apr 4, 2023. It is now read-only.

Rework webApi queries #1111

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
646b338
Add timestamp for webapi
KkevinLi Mar 1, 2019
d36d41c
[query] - Add webQueryInterface to implement database queries through…
KkevinLi Jan 16, 2019
9c193fc
[query] - Rework firebaseWebApi queries to allow chaining of filters.…
KkevinLi Jan 16, 2019
e8406dc
[query] - Rework firebaseWebApi queries to allow chaining of filters.…
KkevinLi Jan 16, 2019
b08f534
[query] - Rework database/index Query class to use the new query api
KkevinLi Jan 16, 2019
8a180c9
[readme] - Added documentation for webApi queries
KkevinLi Jan 16, 2019
b7014ae
[query] - Update web query class to have all filter operations to be …
KkevinLi Mar 5, 2019
f7d813d
Type query.once() to return a promise with datasnapshot
KkevinLi Mar 5, 2019
ebf8ff4
[query][android] - Make webapi queries return a datasnapshot that fol…
KkevinLi Mar 5, 2019
4d6a394
[query][ios] - Make webapi queries return a datasnapshot that follows…
KkevinLi Mar 5, 2019
9d08715
[docs][database] - Update webapi query documentation
KkevinLi Mar 5, 2019
1409b25
[chore] - Remove short references as they are deprecated in later ver…
KkevinLi Mar 5, 2019
a5fcb63
[chore] - White space
KkevinLi Mar 5, 2019
c02d7ae
[chore] - Fix typing errors
KkevinLi Mar 5, 2019
6244c7a
[query] - Update query.on() to follow web api and returns a Function …
KkevinLi Mar 7, 2019
7ffc302
[query] - Accept errorCallback for query.on()
KkevinLi Mar 7, 2019
414197f
[docs][database] - Update webapi queries to show error callbacks
KkevinLi Mar 7, 2019
1e7f884
[query] - Implement query.off correctly to allow removal of callbacks
KkevinLi Oct 3, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions demo/app/vendor-platform.android.ts
Original file line number Diff line number Diff line change
@@ -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");
}
4 changes: 2 additions & 2 deletions demo/app/vendor.ts
Original file line number Diff line number Diff line change
@@ -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();
92 changes: 82 additions & 10 deletions docs/DATABASE.md
Original file line number Diff line number Diff line change
@@ -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':

<details>
<summary>Native API</summary>

```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.
</details>
<details>
<summary>Web API</summary>

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.
</details>

### 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.
<summary>Web API</summary>

```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);
```
</details>

2 changes: 1 addition & 1 deletion src/app/auth/index.ts
Original file line number Diff line number Diff line change
@@ -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 {
217 changes: 89 additions & 128 deletions src/app/database/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, Array<any>> = new Map();
private static registeredCallbacks: Map<string, Array<(a: DataSnapshot | null, b?: string) => 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<DataSnapshot> {
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<DataSnapshot> {
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<any> */
{
export interface ThenableReference extends Reference /*, PromiseLike<any> */ {
}

export class Database {
18 changes: 10 additions & 8 deletions src/app/index.ts
Original file line number Diff line number Diff line change
@@ -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" };
}
2 changes: 1 addition & 1 deletion src/app/storage/index.ts
Original file line number Diff line number Diff line change
@@ -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;
234 changes: 223 additions & 11 deletions src/firebase.android.ts
Original file line number Diff line number Diff line change
@@ -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<any> => {
return new Promise<any>((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<any, Array<any>> = new Map(); // A map to keep track of callbacks to this specific Query Object
private static eventListenerMap: Map<string, Array<any>> = 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<DataSnapshot> {
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),
31 changes: 30 additions & 1 deletion src/firebase.d.ts
Original file line number Diff line number Diff line change
@@ -547,9 +547,36 @@ export interface OnDisconnect {
update(values: Object): Promise<any>;
}

// WebAPI Query
export interface Query {
on(eventType: string, callback: (a: any, b?: string) => any, cancelCallbackOrContext?: (a: Error | null) => any): Function;

once(eventType: string): Promise<DataSnapshot>;

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

/**
190 changes: 189 additions & 1 deletion src/firebase.ios.ts
Original file line number Diff line number Diff line change
@@ -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<any, Array<any>> = new Map(); // A map to keep track of callbacks to this specific Query Object
private static eventListenerMap: Map<string, Array<any>> = 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<DataSnapshot> {
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<any> => {
return new Promise<any>((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),